[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2\njobs:\n  build:\n    docker:\n      - image: circleci/ruby:2.4.1\n    steps:\n      - checkout\n      - run: echo \"A first hello\""
  },
  {
    "path": ".docker/Dockerfile",
    "content": "FROM python:3.6\n# Adapted from tiangolo-uwsgi-flask (https://github.com/tiangolo/uwsgi-nginx-flask-docker) or\n# (https://github.com/tiangolo/uwsgi-nginx-docker/blob/master/python3.6/Dockerfile)\n\nENV NGINX_VERSION 1.13.12-1~stretch\nENV NJS_VERSION   1.13.12.0.2.0-1~stretch\n\n\nRUN set -x \\\n\t&& apt-get update \\\n\t&& apt-get install --no-install-recommends --no-install-suggests -y gnupg1 apt-transport-https ca-certificates \\\n\t&& \\\n\tNGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \\\n\tfound=''; \\\n\tfor server in \\\n\t\tha.pool.sks-keyservers.net \\\n\t\thkp://keyserver.ubuntu.com:80 \\\n\t\thkp://p80.pool.sks-keyservers.net:80 \\\n\t\tpgp.mit.edu \\\n\t; do \\\n\t\techo \"Fetching GPG key $NGINX_GPGKEY from $server\"; \\\n\t\tapt-key adv --keyserver \"$server\" --keyserver-options timeout=10 --recv-keys \"$NGINX_GPGKEY\" && found=yes && break; \\\n\tdone; \\\n\ttest -z \"$found\" && echo >&2 \"error: failed to fetch GPG key $NGINX_GPGKEY\" && exit 1; \\\n\tapt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \\\n\t&& dpkgArch=\"$(dpkg --print-architecture)\" \\\n\t&& nginxPackages=\" \\\n\t\tnginx=${NGINX_VERSION} \\\n\t\" \\\n\t&& case \"$dpkgArch\" in \\\n\t\tamd64|i386) \\\n# arches officialy built by upstream\n\t\t\techo \"deb https://nginx.org/packages/mainline/debian/ stretch nginx\" >> /etc/apt/sources.list.d/nginx.list \\\n\t\t\t&& apt-get update \\\n\t\t\t;; \\\n\t\t*) \\\n# we're on an architecture upstream doesn't officially build for\n# let's build binaries from the published source packages\n\t\t\techo \"deb-src https://nginx.org/packages/mainline/debian/ stretch nginx\" >> /etc/apt/sources.list.d/nginx.list \\\n\t\t\t\\\n# new directory for storing sources and .deb files\n\t\t\t&& tempDir=\"$(mktemp -d)\" \\\n\t\t\t&& chmod 777 \"$tempDir\" \\\n# (777 to ensure APT's \"_apt\" user can access it too)\n\t\t\t\\\n# save list of currently-installed packages so build dependencies can be cleanly removed later\n\t\t\t&& savedAptMark=\"$(apt-mark showmanual)\" \\\n\t\t\t\\\n# build .deb files from upstream's source packages (which are verified by apt-get)\n\t\t\t&& apt-get update \\\n\t\t\t&& apt-get build-dep -y $nginxPackages \\\n\t\t\t&& ( \\\n\t\t\t\tcd \"$tempDir\" \\\n\t\t\t\t&& DEB_BUILD_OPTIONS=\"nocheck parallel=$(nproc)\" \\\n\t\t\t\t\tapt-get source --compile $nginxPackages \\\n\t\t\t) \\\n# we don't remove APT lists here because they get re-downloaded and removed later\n\t\t\t\\\n# reset apt-mark's \"manual\" list so that \"purge --auto-remove\" will remove all build dependencies\n# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies)\n\t\t\t&& apt-mark showmanual | xargs apt-mark auto > /dev/null \\\n\t\t\t&& { [ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark; } \\\n\t\t\t\\\n# create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be)\n\t\t\t&& ls -lAFh \"$tempDir\" \\\n\t\t\t&& ( cd \"$tempDir\" && dpkg-scanpackages . > Packages ) \\\n\t\t\t&& grep '^Package: ' \"$tempDir/Packages\" \\\n\t\t\t&& echo \"deb [ trusted=yes ] file://$tempDir ./\" > /etc/apt/sources.list.d/temp.list \\\n# work around the following APT issue by using \"Acquire::GzipIndexes=false\" (overriding \"/etc/apt/apt.conf.d/docker-gzip-indexes\")\n#   Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)\n#   ...\n#   E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages  Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)\n\t\t\t&& apt-get -o Acquire::GzipIndexes=false update \\\n\t\t\t;; \\\n\tesac \\\n\t\\\n\t&& apt-get install --no-install-recommends --no-install-suggests -y \\\n\t\t\t\t\t\t$nginxPackages \\\n\t\t\t\t\t\tgettext-base \\\n\t&& rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \\\n\t\\\n# if we have leftovers from building, let's purge them (including extra, unnecessary build deps)\n\t&& if [ -n \"$tempDir\" ]; then \\\n\t\tapt-get purge -y --auto-remove \\\n\t\t&& rm -rf \"$tempDir\" /etc/apt/sources.list.d/temp.list; \\\n\tfi\n\n# forward request and error logs to docker log collector\nRUN ln -sf /dev/stdout /var/log/nginx/access.log \\\n\t&& ln -sf /dev/stderr /var/log/nginx/error.log\nEXPOSE 80 443\n\n# Standard set up Nginx finished\n\n# Install uWSGI\nRUN pip install uwsgi\n\n# Make NGINX run on the foreground\nRUN echo \"daemon off;\" >> /etc/nginx/nginx.conf\n# Remove default configuration from Nginx\nRUN rm /etc/nginx/conf.d/default.conf\n# Copy the modified Nginx conf\nCOPY .docker/nginx.conf /etc/nginx/conf.d/\n# Copy the base uWSGI ini file to enable default dynamic uwsgi process number\nCOPY .docker/uwsgi.ini /etc/uwsgi/\n\n# Install Supervisord\nRUN apt-get update && apt-get install -y supervisor sqlite3 libsqlite3-dev uwsgi-plugin-python3 \\\n    && rm -rf /var/lib/apt/lists/*\n# Custom Supervisord config\nCOPY .docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf\n\n\n# Which uWSGI .ini file should be used, to make it customizable\n# ENV UWSGI_INI /commandment/uwsgi.ini\n\n# By default, run 2 processes\nENV UWSGI_CHEAPER 2\n\n# By default, when on demand, run up to 16 processes\nENV UWSGI_PROCESSES 16\n\n# By default, allow unlimited file sizes, modify it to limit the file sizes\n# To have a maximum of 1 MB (Nginx's default) change the line to:\n# ENV NGINX_MAX_UPLOAD 1m\nENV NGINX_MAX_UPLOAD 0\n\n# By default, Nginx will run a single worker process, setting it to auto\n# will create a worker for each CPU core\nENV NGINX_WORKER_PROCESSES 1\n\n# By default, Nginx listens on port 80.\n# To modify this, change LISTEN_PORT environment variable.\n# (in a Dockerfile or with an option for `docker run`)\nENV LISTEN_PORT 80\n\nCOPY . /commandment\nWORKDIR /commandment\nRUN pip install pipenv\nRUN pipenv install --system\nCOPY .docker/uwsgi-commandment.ini /etc/uwsgi/uwsgi-commandment.ini\nCOPY .docker/entry.sh /entry.sh\nCOPY .docker/settings.cfg.docker /settings.cfg\n\nCMD [\"/entry.sh\"]\n"
  },
  {
    "path": ".docker/entry.sh",
    "content": "#!/usr/bin/env bash\n\necho \"Starting commandment...\"\n\nSSL_HOSTNAME=${SSL_HOSTNAME:-\"commandment.test\"}\n\nPYTHONPATH=/commandment\nexport PYTHONPATH\n\n#echo \"Initialising database...\"\n#touch /commandment/commandment.db\n#/usr/local/bin/alembic --config /commandment/alembic.ini -x data=true upgrade head\n\nif [[ ! -f /etc/nginx/ssl/ssl.crt || ! -f /etc/nginx/ssl.key ]]; then\n    echo \"Did not find any SSL certificate to use. SSL is required for MDM.\"\n    echo \"Creating new certificate using environment with DNSName: ${SSL_HOSTNAME}\"\n\n    cat <<- EOF > /tmp/openssl.cnf\n\n        [req]\n        distinguished_name = req_distinguished_name\n        req_extensions = v3_req\n        prompt = no\n        [req_distinguished_name]\n        C = US\n        ST = California\n        L = Cupertino\n        O = Commandment\n        OU = MDM\n        CN = ${SSL_HOSTNAME}\n        [v3_req]\n        # Extensions to add to a certificate request\n        basicConstraints = CA:FALSE\n        keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n        subjectAltName = @alt_names\n        [alt_names]\n        DNS.1 = ${SSL_HOSTNAME}\n        DNS.2 = localhost\nEOF\n\n    openssl req -x509 -nodes -days 730 -newkey rsa:2048 -keyout /etc/nginx/ssl.key -out /etc/nginx/ssl.crt -config /tmp/openssl.cnf -extensions 'v3_req'\n\nfi\n\n\necho \"Starting uWSGI and nginx\"\nexec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf\n\n"
  },
  {
    "path": ".docker/nginx.conf",
    "content": "server {\n    listen 80;\n    listen 443 ssl;\n\n    ssl_certificate ssl.crt;\n    ssl_certificate_key ssl.key;\n    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n\n    root /commandment/commandment/static;\n    index index.html;\n\n    location /api {\n        include uwsgi_params;\n        uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n        uwsgi_pass unix:///tmp/uwsgi.sock;\n    }\n\n    location /enroll {\n        include uwsgi_params;\n        uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n        uwsgi_pass unix:///tmp/uwsgi.sock;\n    }\n\n    location /checkin {\n        include uwsgi_params;\n        uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n        uwsgi_pass unix:///tmp/uwsgi.sock;\n    }\n\n    location /mdm {\n        include uwsgi_params;\n        uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n        uwsgi_pass unix:///tmp/uwsgi.sock;\n    }\n\n    location /scep {\n        include uwsgi_params;\n        uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n        uwsgi_pass unix:///tmp/uwsgi.sock;\n    }\n\n    location /dep {\n        include uwsgi_params;\n        uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n        uwsgi_pass unix:///tmp/uwsgi.sock;\n    }\n\n    location / {\n        try_files $uri /index.html;\n    }\n\n    location /static {\n        alias /commandment/commandment/static;\n    }\n\n}\n"
  },
  {
    "path": ".docker/openssl.cnf",
    "content": "[req]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n\n[req_distinguished_name]\ncountryName = Country Name (2 letter code)\ncountryName_default = AU\nstateOrProvinceName = State or Province Name (full name)\nstateOrProvinceName_default = New South Wales\nlocalityName = Locality Name (eg, city)\nlocalityName_default = Sydney\norganizationalUnitName\t= Organizational Unit Name (eg, section)\norganizationalUnitName_default\t= Domain Control Validated\ncommonName = commandment.test\ncommonName_max\t= 64\n\n[ v3_req ]\n# Extensions to add to a certificate request\nbasicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = localhost\nDNS.2 = mac.local\n"
  },
  {
    "path": ".docker/settings.cfg.docker",
    "content": "from os import path\ndirname = path.dirname(__file__)\n\n# The public facing hostname of the MDM\n# This will also be used as the self signed certificate dnsname\nPUBLIC_HOSTNAME = 'commandment.dev'\n\n# Development mode listen port\nPORT = 5443\n\n# Configure your Database URI.\n# All SQLAlchemy options are available here:\n# http://flask-sqlalchemy.pocoo.org/2.1/config/\nSQLALCHEMY_DATABASE_URI = 'sqlite:////commandment/commandment.db'\n# SQLALCHEMY_DATABASE_ECHO = True\n# SQLALCHEMY_TRACK_MODIFICATIONS = False\n\n# ---------------\n# Certificates\n# ---------------\n\n# [APNS]\n# You may supply the certificate as a pair of PEM encoded files, or as a .p12 container.\n# If you supply .p12 it will be encoded as a PEM keypair\n# -----\n\n# If commandment is running in development mode, specify the path to the certificate and private key.\n# These can also be generated at start up.\n# Normally SSL should be handled by Apache/Nginx/etc.\n\n# [SSL]\n# Specify the Enterprise CA here if Apple Devices won't natively trust your CA eg. If you are using a\n# self-signed CA or Enterprise CA Certificate.\n# -----\nCA_CERTIFICATE = '/etc/nginx/ssl/ca.crt'\n\n# Specify the development web server SSL certificate.\n# This only applies if you are running via the CLI or flask run\n# -----\nSSL_CERTIFICATE = '/etc/nginx/ssl/ssl.crt'\nSSL_RSA_KEY = '/etc/nginx/ssl/ssl.key'\n\n# If not using external storage, the path to the root directory for upload storage.\n# This should not be used in production.\n# -----\nSTORAGE_ROOT = path.join(dirname, 'storage')\n\n# -------------------------\n# SCEP via SCEPy (optional)\n# -------------------------\n\n# Directory where certs, revocation lists, serials etc will be kept\n# -----\nSCEPY_CA_ROOT = \"/path/to/ca\"\n\n# X.509 Name Attributes used to generate the CA Certificate.\n# -----\nSCEPY_CA_X509_CN = 'SCEPY-CA'\nSCEPY_CA_X509_O = 'SCEPy'\nSCEPY_CA_X509_C = 'AU'\n\n# SubjectAltName extension is always on and will use this DNSName\nSAN_DNSNAME = 'scepy.dev'\n\n# (Optional) SCEP static challenge. This will have to be part of your SCEP profile\n# -----\nSCEPY_CHALLENGE = 'sekret'\n\n# Raw data will be dumped to this directory for inspection with tools such as OpenSSL (openssl asn1parse)\n# -----\nSCEPY_DUMP_DIR = '/tmp/scepy_dump'\n\n# If the GetCACert would return a single cert, force it to use a CMS degenerate case?\n# -----\nSCEPY_FORCE_DEGENERATE_FOR_SINGLE_CERT = False\n"
  },
  {
    "path": ".docker/supervisord.conf",
    "content": "[supervisord]\nnodaemon=true\n\n[program:uwsgi]\ncommand=/usr/local/bin/uwsgi --ini /etc/uwsgi/uwsgi.ini --ini /etc/uwsgi/uwsgi-commandment.ini --die-on-term --need-app\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:nginx]\ncommand=/usr/sbin/nginx\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n# Graceful stop, see http://nginx.org/en/docs/control.html\nstopsignal=QUIT\n"
  },
  {
    "path": ".docker/uwsgi-commandment.ini",
    "content": "[uwsgi]\nbase = /commandment\n\npythonpath = %(base)\nmodule = commandment:create_app()\nplugins = python3\n\nenv = COMMANDMENT_SETTINGS=/settings.cfg\n\nmaster = true\nprocesses = 4\nenable-threads = true\ndie-on-term = true\n"
  },
  {
    "path": ".docker/uwsgi.ini",
    "content": "[uwsgi]\nsocket = /tmp/uwsgi.sock\nchown-socket = nginx:nginx\nchmod-socket = 664\n# Graceful shutdown on SIGTERM, see https://github.com/unbit/uwsgi/issues/849#issuecomment-118869386\nhook-master-start = unix_signal:15 gracefully_kill_them_all\n"
  },
  {
    "path": ".dockerignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n### JetBrains template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff:\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/dictionaries\n\n# Sensitive or high-churn files:\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.xml\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n\n# Gradle:\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Mongo Explorer plugin:\n.idea/**/mongoSettings.xml\n\n## File-based project format:\n*.iws\n\n## Plugin-specific files:\n\n# IntelliJ\n/out/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n### Node template\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# Not relevant to running environment\nassets\ndoc\ntests\ntestdata\n\n# ui output should already be in commandment/static\nui\n\n# dont use dev local settings\nsettings.cfg\n\n# dont copy dev db into context\n*.db\n\n# no certificate(s)\n*.cer\n*.crt\n*.p12\n*.key\n*.pem\nssl\n\n# no simulator(s)\nsimulators\n\n.git\n"
  },
  {
    "path": ".gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndoc/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n### JetBrains template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff:\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/dictionaries\n\n# Sensitive or high-churn files:\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.xml\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n\n# Gradle:\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Mongo Explorer plugin:\n.idea/**/mongoSettings.xml\n\n## File-based project format:\n*.iws\n\n## Plugin-specific files:\n\n# IntelliJ\n/out/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# IDEA\n*.iml\n.idea\n\n# SQLite3\n*.db\n*.db-journal\n\n# Flask settings.cfg\nsettings.cfg\n\n\n# dont ever commit certs\n*.pem\n*.p12\n*.cer\n*.crt\n*.key\n*.csr\n*.p7m\n\n# dont ever commit vpp tokens if they are downloaded into the root\n*.vpptoken\ndeptoken.json\n\n# mypy\n.mypy_cache\n\n# npm lock\npackage-lock.json\n"
  },
  {
    "path": ".gitlab-ci.yml",
    "content": "image: python:3.6.5-stretch\n\nvariables:\n    PIP_CACHE_DIR: \"$CI_PROJECT_DIR/.cache\"\n\n#cache:\n#  key: ${CI_COMMIT_REF_SLUG}\n#  paths:\n#    - node_modules/\n#\n\nbuild_python:\n  stage: build\n#  tags:\n#    - python\n  before_script:\n    - python -V\n#    - pip install virtualenv\n#    - virtualenv venv\n#    - source venv/bin/activate\n  script:\n#    - apt-get update -qy\n#    - apt-get install -y python-dev python-pip\n    - pip install pipenv\n    - pipenv install --system --dev\n  cache:\n    key: backend\n    paths:\n      - .cache/\n      - venv/\n      - /root/.local/share/virtualenvs/\n\nbuild_js:\n  stage: build\n#  tags:\n#    - js\n  script:\n    - apt-get update -qy\n    - apt-get install -y apt-transport-https\n    - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -\n    - echo \"deb https://dl.yarnpkg.com/debian/ stable main\" | tee /etc/apt/sources.list.d/yarn.list\n    - curl -sL https://deb.nodesource.com/setup_8.x | bash -\n    - apt-get update -qy\n    - apt-get install -y yarn\n    - cd ui && yarn install\n    - NODE_ENV=production ./node_modules/.bin/webpack\n  artifacts:\n    paths:\n      - commandment/static/\n  cache:\n    key: frontend\n    paths:\n      - node_modules/\n\ntest_python:\n  stage: test\n#  tags:\n#    - python\n  before_script:\n    - pip install pipenv\n    - pipenv install --system --dev\n  script:\n    - pytest -v -m \"not depsim and not dep and not vppsim and not vpp\" tests\n  cache:\n    key: backend\n    paths:\n      - .cache/\n      - venv/\n      - /root/.local/share/virtualenvs/\n\n\n    "
  },
  {
    "path": "LICENSE.txt",
    "content": "Copyright (c) 2015 Jesse Peterson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "Pipfile",
    "content": "[[source]]\nurl = \"https://pypi.python.org/simple\"\nverify_ssl = true\nname = \"pypi\"\n\n[requires]\npython_version = \"3.6\"\n\n[packages]\nacme = \"*\"\naiohttp = \"*\"\nalembic = \"*\"\n\"aniso8601\" = \"*\"\napns = \"*\"\n\"apns2-client\" = \"*\"\nbabel = \"*\"\nbiplist = \"*\"\nbixar = {git = \"https://github.com/cmdmnt/bixar.git\",editable = true}\nblinker = \"*\"\ndecorator = \"*\"\ndocutils = \"*\"\nflask = \"*\"\nflask-alembic = \"*\"\nflask-cors = \"*\"\nflask-jwt = \"*\"\nflask-marshmallow = \"*\"\nflask-rest-jsonapi = \"*\"\nflask-restful = \"*\"\nflask-sqlalchemy = \"*\"\nfuture = \"*\"\nimagesize = \"*\"\nmarshmallow-enum = \"*\"\nmarshmallow-sqlalchemy = \"*\"\n\"oauth2\" = \"*\"\n\"oauth2client\" = \"*\"\noscrypto = \"*\"\npasslib = \"*\"\npaste = \"*\"\npy = \"*\"\n\"pyasn1\" = \"*\"\n\"pyasn1-modules\" = \"*\"\npycparser = \"*\"\npycryptodomex = \"*\"\npygments = \"*\"\npyparsing = \"*\"\n\"repoze.who\" = \"*\"\nrequests = \"*\"\nrsa = \"*\"\nSCEPy = {git = \"https://github.com/cmdmnt/SCEPy.git\",editable = true}\nsemver = \"*\"\nsignxml = \"*\"\nukpostcodeparser = \"*\"\nsqlalchemy = \"*\"\ncryptography = \"*\"\npython-dateutil = \"*\"\nrequests-oauthlib = \"*\"\nauthlib = \"*\"\ncommandment = {editable = true,path = \".\"}\n\n[dev-packages]\nalembic-viz = \"*\"\nfactory-boy = \"*\"\nmock = \"*\"\nmypy = \"*\"\npytest = \"*\"\npytest-runner = \"*\"\nsadisplay = \"*\"\nsphinx = \"*\"\nsphinxcontrib-httpdomain = \"*\"\nsphinxcontrib-napoleon = \"*\"\nsphinxcontrib-plantuml = \"*\"\nsphinxcontrib-websupport = \"*\"\nguzzle-sphinx-theme = \"*\"\ntyping = \"*\"\nsphinx-rtd-theme = \"*\"\n"
  },
  {
    "path": "README.rst",
    "content": "===========================\nCommandment Open Source MDM\n===========================\n\n.. image:: https://travis-ci.org/cmdmnt/commandment.svg?branch=master\n   :target: https://travis-ci.org/cmdmnt/commandment\n\nCommandment is an Open Source Apple MDM with support for managing iOS and macOS devices.\n\nThe source code is available under an `MIT license <LICENSE.txt>`_.\n\n------------\nRequirements\n------------\n\n* Apple MDM Push Certificate and private key (in PEM format)\n  * Obtain a free Push Certificate from `mdmcert.download <https://mdmcert.download>`_.\n  * Alternatively requires an Apple Enterprise Developer account (US$300/year) with the MDM vendor option enabled.\n* A trusted TLS certificate for the MDM.\n* `Python 3.6+ <https://www.python.org/>`_\n\n-------------\nDocumentation\n-------------\n\nThe user, developer and API documentation is available at:\n\nhttp://cmdmnt.github.io/commandment/\n\n------------------\nBugs, issues, etc.\n------------------\n\nPlease report any issues, bugs, suggestions, feedback, etc. \nto the `issue tracker <https://github.com/cmdmnt/commandment/issues>`_ of this project.\n\nAlso for discussion, and support, join us in the #commandment channel in the `MacAdmins Slack <http://macadmins.herokuapp.com/>`_ !\n\n"
  },
  {
    "path": "alembic.ini",
    "content": "[alembic]\n# path to migration scripts\nscript_location = %(here)s/commandment/alembic\n\nsqlalchemy.url = sqlite:///commandment.db\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = DEBUG\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = INFO\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = DEBUG\nhandlers =\nqualname = alembic\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "commandment/__init__.py",
    "content": "\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2017 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\"\"\"\nfrom typing import Union, Optional\nfrom pathlib import PurePath\nfrom flask import Flask, render_template\n\nfrom commandment.mdm.app import mdm_app\nfrom .ac2.ac2_app import ac2_app\nfrom .api.app_jsonapi import api_app, api\nfrom .api.app_json import flat_api\nfrom .apns.app import api_push_app\nfrom .auth.app import oauth_app\nfrom .auth import oauth2\nfrom .api.configuration import configuration_app\nfrom .enroll.app import enroll_app\nfrom .models import db\nfrom .omdm import omdm_app\nfrom .dep.app import dep_app\nfrom .vpp.app import vpp_app\nfrom .profiles.api import profiles_api_app\nfrom .inventory.api import api_app as inventory_api\nfrom .mdm.api import api_app as mdm_api\nfrom .apps.app_jsonapi import api_app as applications_api\n\nfrom .threads import startup_thread\nfrom .dep import threads as dep_threads\nfrom .apns import threads as push_threads\n\n\ndef create_app(config_file: Optional[Union[str, PurePath]] = None) -> Flask:\n    \"\"\"Create the Flask Application.\n\n    Configuration is looked up the following order:\n\n    - default_settings.py in the commandment package.\n    - config_file parameter passed to this factory method.\n    - environment variable ``COMMANDMENT_SETTINGS`` pointing to a .cfg file.\n\n    Args:\n        config_file (Union[str, PurePath]): Path to configuration file.\n\n    Returns:\n        Instance of the flask application\n    \"\"\"\n    app = Flask(__name__)\n    app.config.from_object('commandment.default_settings')\n    if config_file is not None:\n        app.config.from_pyfile(config_file)\n    else:\n        app.config.from_envvar('COMMANDMENT_SETTINGS')\n\n    db.init_app(app)\n    oauth2.init_app(app)\n    api.init_app(app)\n    api.oauth_manager(oauth2.require_oauth)\n\n    app.register_blueprint(oauth_app, url_prefix='/oauth')\n    app.register_blueprint(enroll_app, url_prefix='/enroll')\n    app.register_blueprint(mdm_app)\n    app.register_blueprint(configuration_app, url_prefix='/api/v1/configuration')\n    app.register_blueprint(api_app, url_prefix='/api')\n    app.register_blueprint(api_push_app, url_prefix='/api')\n    app.register_blueprint(flat_api, url_prefix='/api')\n    app.register_blueprint(profiles_api_app, url_prefix='/api')\n    app.register_blueprint(applications_api, url_prefix='/api')\n    app.register_blueprint(omdm_app, url_prefix='/omdm')\n    app.register_blueprint(ac2_app)\n    app.register_blueprint(dep_app)\n    app.register_blueprint(vpp_app)\n\n    try:\n        from scepy.blueprint import scep_app\n        app.register_blueprint(scep_app, url_prefix='/scep')\n        app.logger.info('Registered SCEPy service at /scep')\n    except ImportError:\n        app.logger.warning(\"SCEP will not be available, cannot load SCEPy\")\n\n    # Threads\n    startup_thread.start(app)\n    # dep_threads.start(app)\n    # push_threads.start(app)\n\n    # SPA Entry Point (when not behind nginx or apache)\n    @app.route('/')\n    def index():\n        \"\"\"Main entry point for the administrator web application.\"\"\"\n        return render_template('index.html')\n\n    # SPA history fallback handler\n    @app.errorhandler(404)\n    def send_index(path: str):\n        \"\"\"Fallback route for HTML5 History.\"\"\"\n        return render_template('index.html')\n\n    return app\n\n"
  },
  {
    "path": "commandment/ac2/__init__.py",
    "content": ""
  },
  {
    "path": "commandment/ac2/ac2_app.py",
    "content": "from flask import Blueprint, jsonify, current_app\n\nac2_app = Blueprint('ac2_app', __name__)\n\n\n@ac2_app.route('/MDMServiceConfig')\ndef mdm_service_config():\n    \"\"\"Apple Configurator 2 checks this route to figure out which enrollment profile it should use.\"\"\"\n    public_hostname = current_app.config.get('PUBLIC_HOSTNAME', 'localhost')\n    port = current_app.config.get('PORT', 443)\n\n    return jsonify({\n        'dep_enrollment_url': 'https://{}:{}/dep/profile'.format(public_hostname, port),\n        'dep_anchor_certs_url': 'https://{}:{}/dep/anchor_certs'.format(public_hostname, port),\n        'trust_profile_url': 'https://{}:{}/enroll/trust.mobileconfig'.format(public_hostname, port)\n    })\n"
  },
  {
    "path": "commandment/alembic/__init__.py",
    "content": ""
  },
  {
    "path": "commandment/alembic/disabled_versions/072fba4a2256_create_ad_payload_table.py",
    "content": "\"\"\"Create ad_payload table\n\nRevision ID: 072fba4a2256\nRevises: 8186b8ecf0fc\nCreate Date: 2017-05-19 19:50:25.537513\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '072fba4a2256'\ndown_revision = '8186b8ecf0fc'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('ad_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('host_name', sa.String(), nullable=False),\n                    sa.Column('user_name', sa.String(), nullable=False),\n                    sa.Column('password', sa.String(), nullable=False),\n                    sa.Column('ad_organizational_unit', sa.String(), nullable=False),\n                    sa.Column('ad_mount_style', sa.Enum('AFP', 'SMB', name='admountstyle'), nullable=False),\n                    sa.Column('ad_default_user_shell', sa.String(), nullable=True),\n                    sa.Column('ad_map_uid_attribute', sa.String(), nullable=True),\n                    sa.Column('ad_map_gid_attribute', sa.String(), nullable=True),\n                    sa.Column('ad_map_ggid_attribute', sa.String(), nullable=True),\n                    sa.Column('ad_preferred_dc_server', sa.String(), nullable=True),\n                    sa.Column('ad_domain_admin_group_list', sa.String(), nullable=True),\n                    sa.Column('ad_namespace', sa.Enum('Domain', 'Forest', name='adnamespace'), nullable=True),\n                    sa.Column('ad_packet_sign', sa.Enum('Allow', 'Disable', 'Require', name='adpacketsignpolicy'), nullable=True),\n                    sa.Column('ad_packet_encrypt', sa.Enum('Allow', 'Disable', 'Require', 'SSL', name='adpacketencryptpolicy'), nullable=True),\n                    sa.Column('ad_restrict_ddns', sa.String(), nullable=True),\n                    sa.Column('ad_trust_change_pass_interval', sa.Integer(), nullable=True),\n                    sa.Column('ad_create_mobile_account_at_login', sa.Boolean(), nullable=True),\n                    sa.Column('ad_warn_user_before_creating_ma', sa.Boolean(), nullable=True),\n                    sa.Column('ad_force_home_local', sa.Boolean(), nullable=True),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('ad_payload')\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/18412434fb57_create_energy_saver_payload_table.py",
    "content": "\"\"\"Create energy_saver_payload table\n\nRevision ID: 18412434fb57\nRevises: 323a90039a6a\nCreate Date: 2017-05-19 19:53:03.142964\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = '18412434fb57'\ndown_revision = '323a90039a6a'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('energy_saver_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('destroy_fv_key_on_standby', sa.Boolean(), nullable=True),\n                    sa.Column('sleep_disabled', sa.Boolean(), nullable=True),\n                    sa.Column('desktop_acpower_profilenumber', sa.Integer(), nullable=True),\n                    sa.Column('portable_acpower_profilenumber', sa.Integer(), nullable=True),\n                    sa.Column('portable_battery_profilenumber', sa.Integer(), nullable=True),\n                    sa.Column('desktop_acpower', commandment.dbtypes.JSONEncodedDict(), nullable=True),\n                    sa.Column('portable_acpower', commandment.dbtypes.JSONEncodedDict(), nullable=True),\n                    sa.Column('portable_battery', commandment.dbtypes.JSONEncodedDict(), nullable=True),\n                    sa.Column('desktop_schedule', commandment.dbtypes.JSONEncodedDict(), nullable=True),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('energy_saver_payload')\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/323a90039a6a_create_email_payload_table.py",
    "content": "\"\"\"Create email_payload table\n\nRevision ID: 323a90039a6a\nRevises: e47e29a9537c\nCreate Date: 2017-05-19 19:52:05.726744\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '323a90039a6a'\ndown_revision = 'e47e29a9537c'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('email_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('email_account_description', sa.String(), nullable=True),\n                    sa.Column('email_account_name', sa.String(), nullable=True),\n                    sa.Column('email_account_type', sa.Enum('POP', 'IMAP', name='emailaccounttype'), nullable=False),\n                    sa.Column('email_address', sa.String(), nullable=True),\n                    sa.Column('incoming_auth', sa.Enum('Password', 'CRAM_MD5', 'NTLM', 'HTTP_MD5', 'ENone', name='emailauthenticationtype'), nullable=False),\n                    sa.Column('incoming_host', sa.String(), nullable=False),\n                    sa.Column('incoming_port', sa.Integer(), nullable=True),\n                    sa.Column('incoming_use_ssl', sa.Boolean(), nullable=True),\n                    sa.Column('incoming_username', sa.String(), nullable=False),\n                    sa.Column('incoming_password', sa.String(), nullable=True),\n                    sa.Column('outgoing_password', sa.String(), nullable=True),\n                    sa.Column('outgoing_incoming_same', sa.Boolean(), nullable=True),\n                    sa.Column('outgoing_auth', sa.Enum('Password', 'CRAM_MD5', 'NTLM', 'HTTP_MD5', 'ENone', name='emailauthenticationtype'), nullable=False),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('email_payload')\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/4eddbcb30464_create_mdm_payload_table.py",
    "content": "\"\"\"Create mdm_payload table\n\nRevision ID: 4eddbcb30464\nRevises: 18412434fb57\nCreate Date: 2017-05-19 19:54:24.264198\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = '4eddbcb30464'\ndown_revision = '18412434fb57'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('mdm_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('identity_certificate_uuid', commandment.dbtypes.GUID(), nullable=False),\n                    sa.Column('topic', sa.String(), nullable=False),\n                    sa.Column('server_url', sa.String(), nullable=False),\n                    sa.Column('server_capabilities', sa.String(), nullable=True),\n                    sa.Column('sign_message', sa.Boolean(), nullable=True),\n                    sa.Column('check_in_url', sa.String(), nullable=True),\n                    sa.Column('check_out_when_removed', sa.Boolean(), nullable=True),\n                    sa.Column('access_rights', sa.Integer(), nullable=True),\n                    sa.Column('use_development_apns', sa.Boolean(), nullable=True),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('mdm_payload')\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/8186b8ecf0fc_create_ad_cert_payload_table.py",
    "content": "\"\"\"Create ad_cert_payload table\n\nRevision ID: 8186b8ecf0fc\nRevises: 13358fb3846b\nCreate Date: 2017-05-19 19:49:07.136996\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8186b8ecf0fc'\ndown_revision = '13358fb3846b'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('ad_cert_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('certificate_description', sa.String(), nullable=True),\n                    sa.Column('allow_all_apps_access', sa.Boolean(), nullable=True),\n                    sa.Column('cert_server', sa.String(), nullable=False),\n                    sa.Column('cert_template', sa.String(), nullable=False),\n                    sa.Column('acquisition_mechanism', sa.Enum('RPC', 'HTTP', name='adcertificateacquisitionmechanism'), nullable=True),\n                    sa.Column('certificate_authority', sa.String(), nullable=False),\n                    sa.Column('renewal_time_interval', sa.Integer(), nullable=True),\n                    sa.Column('identity_description', sa.String(), nullable=True),\n                    sa.Column('key_is_extractable', sa.Boolean(), nullable=True),\n                    sa.Column('prompt_for_credentials', sa.Boolean(), nullable=True),\n                    sa.Column('keysize', sa.Integer(), nullable=True),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('ad_cert_payload')\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/9dd4e48235e3_create_vpn_payload_table.py",
    "content": "\"\"\"Create vpn_payload table\n\nRevision ID: 9dd4e48235e3\nRevises: e5840df9a88a\nCreate Date: 2017-05-19 19:59:55.582629\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '9dd4e48235e3'\ndown_revision = 'e5840df9a88a'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('vpn_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('user_defined_name', sa.String(), nullable=True),\n                    sa.Column('override_primary', sa.Boolean(), nullable=True),\n                    sa.Column('vpn_type', sa.Enum('L2TP', 'PPTP', 'IPSec', 'IKEv2', 'AlwaysOn', 'VPN', name='vpntype'), nullable=False),\n                    sa.Column('vpn_sub_type', sa.String(), nullable=True),\n                    sa.Column('provider_bundle_identifier', sa.String(), nullable=True),\n                    sa.Column('on_demand_enabled', sa.Integer(), nullable=True),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('vpn_payload')\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/d65049bf4b91_create_wifi_payload_table.py",
    "content": "\"\"\"Create wifi_payload table\n\nRevision ID: d65049bf4b91\nRevises: 9dd4e48235e3\nCreate Date: 2017-05-19 20:00:36.548840\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = 'd65049bf4b91'\ndown_revision = '9dd4e48235e3'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('wifi_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('ssid_str', sa.String(), nullable=False),\n                    sa.Column('hidden_network', sa.Boolean(), nullable=True),\n                    sa.Column('auto_join', sa.Boolean(), nullable=True),\n                    sa.Column('encryption_type', sa.Enum('ENone', 'Any', 'WPA2', 'WPA', 'WEP', name='wifiencryptiontype'), nullable=True),\n                    sa.Column('is_hotspot', sa.Boolean(), nullable=True),\n                    sa.Column('domain_name', sa.String(), nullable=True),\n                    sa.Column('service_provider_roaming_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('roaming_consortium_ois', sa.String(), nullable=True),\n                    sa.Column('nai_realm_names', sa.String(), nullable=True),\n                    sa.Column('mccs_and_mncs', sa.String(), nullable=True),\n                    sa.Column('displayed_operator_name', sa.String(), nullable=True),\n                    sa.Column('captive_bypass', sa.Boolean(), nullable=True),\n                    sa.Column('password', sa.String(), nullable=True),\n                    sa.Column('tls_certificate_required', sa.Boolean(), nullable=True),\n                    sa.Column('payload_certificate_uuid', commandment.dbtypes.GUID(), nullable=True),\n                    sa.Column('proxy_type', sa.String(), nullable=True),\n                    sa.Column('proxy_server', sa.String(), nullable=True),\n                    sa.Column('proxy_server_port', sa.Integer(), nullable=True),\n                    sa.Column('proxy_username', sa.String(), nullable=True),\n                    sa.Column('proxy_password', sa.String(), nullable=True),\n                    sa.Column('proxy_pac_url', sa.String(), nullable=True),\n                    sa.Column('proxy_pac_fallback_allowed', sa.Boolean(), nullable=True),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('wifi_payload')\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/da52b64b865f_create_apps_table.py",
    "content": "\"\"\"Create apps table\n\nRevision ID: da52b64b865f\nRevises:\nCreate Date: 2017-05-18 22:27:44.830159\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'da52b64b865f'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('apps',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('filename', sa.String(), nullable=False),\n                    sa.Column('filesize', sa.Integer(), nullable=False),\n                    sa.Column('md5_hash', sa.String(length=32), nullable=False),\n                    sa.Column('md5_chunk_size', sa.Integer(), nullable=False),\n                    sa.Column('md5_chunk_hashes', sa.Text(), nullable=True),\n                    sa.Column('bundle_ids_json', sa.Text(), nullable=True),\n                    sa.Column('pkg_ids_json', sa.Text(), nullable=True),\n                    sa.PrimaryKeyConstraint('id'),\n                    sa.UniqueConstraint('filename')\n                    )\n\n\ndef downgrade():\n    op.drop_table('app')\n\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/e47e29a9537c_create_certificate_payload_table.py",
    "content": "\"\"\"Create certificate_payload table\n\nRevision ID: e47e29a9537c\nRevises: 072fba4a2256\nCreate Date: 2017-05-19 19:51:20.672688\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'e47e29a9537c'\ndown_revision = '072fba4a2256'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('certificate_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('certificate_file_name', sa.String(), nullable=True),\n                    sa.Column('payload_content', sa.LargeBinary(), nullable=True),\n                    sa.Column('password', sa.String(), nullable=True),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('certificate_payload')\n"
  },
  {
    "path": "commandment/alembic/disabled_versions/fc0c134cbb2e_create_password_policy_payload_table.py",
    "content": "\"\"\"Create password_policy_payload table\n\nRevision ID: fc0c134cbb2e\nRevises: 4eddbcb30464\nCreate Date: 2017-05-19 19:56:45.009648\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'fc0c134cbb2e'\ndown_revision = '4eddbcb30464'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('password_policy_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('allow_simple', sa.Boolean(), nullable=True),\n                    sa.Column('force_pin', sa.Boolean(), nullable=True),\n                    sa.Column('max_failed_attempts', sa.Integer(), nullable=True),\n                    sa.Column('max_inactivity', sa.Integer(), nullable=True),\n                    sa.Column('max_pin_age_in_days', sa.Integer(), nullable=True),\n                    sa.Column('min_complex_chars', sa.Integer(), nullable=True),\n                    sa.Column('min_length', sa.Integer(), nullable=True),\n                    sa.Column('require_alphanumeric', sa.Boolean(), nullable=True),\n                    sa.Column('pin_history', sa.Integer(), nullable=True),\n                    sa.Column('max_grace_period', sa.Integer(), nullable=True),\n                    sa.Column('allow_fingerprint_modification', sa.Boolean(), nullable=True),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('password_policy_payload')\n"
  },
  {
    "path": "commandment/alembic/env.py",
    "content": "from __future__ import with_statement\nfrom alembic import context\nfrom sqlalchemy import engine_from_config, pool\nfrom logging.config import fileConfig\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\n\n# add your model's MetaData object here\n# for 'autogenerate' support\nfrom commandment.models import db\n# import commandment.vpp.models\n#import commandment.dep.models\n#import commandment.apps.models\n#import commandment.pki.models\nimport commandment.auth.models\ntarget_metadata = db.metadata\n\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url, target_metadata=target_metadata, literal_binds=True)\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n    connectable = config.attributes.get('connection', None)\n\n    if connectable is None:\n        # only create Engine if we don't have a Connection\n        # from the outside\n        connectable = engine_from_config(\n            config.get_section(config.config_ini_section),\n            prefix='sqlalchemy.',\n            poolclass=pool.NullPool)\n\n    # when connectable is already a Connection object, calling\n    # connect() gives us a *branched connection*.\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            render_as_batch=True,\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "commandment/alembic/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n${imports if imports else \"\"}\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    schema_upgrades()\n    if context.get_x_argument(as_dictionary=True).get('data', None):\n        data_upgrades()\n\n\ndef downgrade():\n    if context.get_x_argument(as_dictionary=True).get('data', None):\n        data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    ${downgrades if downgrades else \"pass\"}\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/0201b96ab856_add_ios_available_os_updates_fields.py",
    "content": "\"\"\"add ios available os updates fields\n\nRevision ID: 0201b96ab856\nRevises: e947cdf82307\nCreate Date: 2018-07-01 21:37:27.355712\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '0201b96ab856'\ndown_revision = 'e947cdf82307'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.add_column('available_os_updates', sa.Column('build', sa.String(), nullable=True))\n    op.add_column('available_os_updates', sa.Column('download_size', sa.BigInteger(), nullable=True))\n    op.add_column('available_os_updates', sa.Column('install_size', sa.BigInteger(), nullable=True))\n    op.add_column('available_os_updates', sa.Column('product_name', sa.String(), nullable=True))\n\n\ndef schema_downgrades():\n    op.drop_column('available_os_updates', 'product_name')\n    op.drop_column('available_os_updates', 'install_size')\n    op.drop_column('available_os_updates', 'download_size')\n    op.drop_column('available_os_updates', 'build')\n\n\n# def data_upgrades():\n#     \"\"\"Add any optional data upgrade migrations here!\"\"\"\n#     pass\n#\n#\n# def data_downgrades():\n#     \"\"\"Add any optional data downgrade migrations here!\"\"\"\n#     pass\n"
  },
  {
    "path": "commandment/alembic/versions/0ab46b2f6d8c_create_users_table.py",
    "content": "\"\"\"Create users table\n\nRevision ID: 0ab46b2f6d8c\nRevises: f5237c7e2374\nCreate Date: 2017-05-19 19:35:12.126022\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '0ab46b2f6d8c'\ndown_revision = 'f5237c7e2374'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('users',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('name', sa.String(), nullable=True),\n                    sa.Column('fullname', sa.String(), nullable=True),\n                    sa.Column('password', sa.String(), nullable=True),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('users')\n"
  },
  {
    "path": "commandment/alembic/versions/0c4c448f4daf_create_device_users_table.py",
    "content": "\"\"\"Create device_users table\n\nRevision ID: 0c4c448f4daf\nRevises: 7d578eb75092\nCreate Date: 2017-05-18 22:32:52.087025\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\n# revision identifiers, used by Alembic.\nrevision = '0c4c448f4daf'\ndown_revision = '7d578eb75092'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('device_users',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('udid', commandment.dbtypes.GUID(), nullable=False),\n                    sa.Column('user_id', commandment.dbtypes.GUID(), nullable=False),\n                    sa.Column('long_name', sa.String(), nullable=True),\n                    sa.Column('short_name', sa.String(), nullable=True),\n                    sa.Column('need_sync_response', sa.Boolean(), nullable=True),\n                    sa.Column('user_configuration', sa.Boolean(), nullable=True),\n                    sa.Column('digest_challenge', sa.String(), nullable=True),\n                    sa.Column('auth_token', sa.String(), nullable=True),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('device_users')\n"
  },
  {
    "path": "commandment/alembic/versions/0e5babc5b9ee_create_vpp_licenses.py",
    "content": "\"\"\"Create vpp_licenses table\n\nRevision ID: 0e5babc5b9ee\nRevises: 875dcce0bf8b\nCreate Date: 2017-07-19 12:56:55.273155\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '0e5babc5b9ee'\ndown_revision = '875dcce0bf8b'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.create_table('vpp_licenses',\n    sa.Column('license_id', sa.Integer(), nullable=False),\n    sa.Column('adam_id', sa.String(), nullable=True),\n    sa.Column('product_type', sa.Enum('Software', 'Application', 'Publication', name='vppproducttype'), nullable=True),\n    sa.Column('product_type_name', sa.String(), nullable=True),\n    sa.Column('pricing_param', sa.Enum('StandardQuality', 'HighQuality', name='vpppricingparam'), nullable=True),\n    sa.Column('is_irrevocable', sa.Boolean(), nullable=True),\n    sa.Column('user_id', sa.Integer(), nullable=True),\n    sa.Column('client_user_id', commandment.dbtypes.GUID(), nullable=True),\n    sa.Column('its_id_hash', sa.String(), nullable=True),\n    sa.ForeignKeyConstraint(['client_user_id'], ['vpp_users.client_user_id'], ),\n    sa.ForeignKeyConstraint(['user_id'], ['vpp_users.user_id'], ),\n    sa.PrimaryKeyConstraint('license_id')\n    )\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_table('vpp_licenses')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/1005dc7dea01_os_update_settings.py",
    "content": "\"\"\"os_update_settings\n\nRevision ID: 1005dc7dea01\nRevises: b74ca08cfd9a\nCreate Date: 2018-02-02 15:49:22.170956\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '1005dc7dea01'\ndown_revision = 'b74ca08cfd9a'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.add_column('devices', sa.Column('osu_automatic_app_installation_enabled', sa.Boolean(), nullable=True))\n    op.add_column('devices', sa.Column('osu_automatic_check_enabled', sa.Boolean(), nullable=True))\n    op.add_column('devices', sa.Column('osu_automatic_os_installation_enabled', sa.Boolean(), nullable=True))\n    op.add_column('devices', sa.Column('osu_automatic_security_updates_enabled', sa.Boolean(), nullable=True))\n    op.add_column('devices', sa.Column('osu_background_download_enabled', sa.Boolean(), nullable=True))\n    op.add_column('devices', sa.Column('osu_catalog_url', sa.String(), nullable=True))\n    op.add_column('devices', sa.Column('osu_is_default_catalog', sa.Boolean(), nullable=True))\n    op.add_column('devices', sa.Column('osu_perform_periodic_check', sa.Boolean(), nullable=True))\n    op.add_column('devices', sa.Column('osu_previous_scan_date', sa.DateTime(), nullable=True))\n    op.add_column('devices', sa.Column('osu_previous_scan_result', sa.String(), nullable=True))\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_column('devices', 'osu_previous_scan_result')\n    op.drop_column('devices', 'osu_previous_scan_date')\n    op.drop_column('devices', 'osu_perform_periodic_check')\n    op.drop_column('devices', 'osu_is_default_catalog')\n    op.drop_column('devices', 'osu_catalog_url')\n    op.drop_column('devices', 'osu_background_download_enabled')\n    op.drop_column('devices', 'osu_automatic_security_updates_enabled')\n    op.drop_column('devices', 'osu_automatic_os_installation_enabled')\n    op.drop_column('devices', 'osu_automatic_check_enabled')\n    op.drop_column('devices', 'osu_automatic_app_installation_enabled')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/13358fb3846b_create_subject_alternative_names_table.py",
    "content": "\"\"\"Create subject_alternative_names table\n\nRevision ID: 13358fb3846b\nRevises: ea34ae3f1e7e\nCreate Date: 2017-05-19 19:48:09.977131\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '13358fb3846b'\ndown_revision = 'ea34ae3f1e7e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('subject_alternative_names',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('discriminator', sa.Enum('RFC822Name', 'DNSName', 'UniformResourceIdentifier', 'DirectoryName', 'RegisteredID', 'IPAddress', 'OtherName', name='subjectalternativenametype'), nullable=False),\n                    sa.Column('str_value', sa.String(), nullable=True),\n                    sa.Column('octet_value', sa.LargeBinary(), nullable=True),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('subject_alternative_names')\n"
  },
  {
    "path": "commandment/alembic/versions/1532dff16984_drop_device_groups.py",
    "content": "\"\"\"drop device groups\n\nRevision ID: 1532dff16984\nRevises: f8eb70b3aa2b\nCreate Date: 2018-03-13 21:26:13.058020\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '1532dff16984'\ndown_revision = 'f8eb70b3aa2b'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.drop_table('device_groups')\n    op.drop_table('device_group_devices')\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.create_table('device_group_devices',\n                    sa.Column('device_group_id', sa.INTEGER(), nullable=False),\n                    sa.Column('device_id', sa.INTEGER(), nullable=False),\n                    sa.ForeignKeyConstraint(['device_group_id'], ['device_groups.id'], ),\n                    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ),\n                    sa.PrimaryKeyConstraint('device_group_id', 'device_id')\n                    )\n    op.create_table('device_groups',\n                    sa.Column('id', sa.INTEGER(), nullable=False),\n                    sa.Column('name', sa.VARCHAR(), nullable=False),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/2808deb9fc62_create_dep_configurations.py",
    "content": "\"\"\"Create DEP configurations\n\nRevision ID: 2808deb9fc62\nRevises: 0201b96ab856\nCreate Date: 2018-07-04 16:57:16.899029\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '2808deb9fc62'\ndown_revision = '0201b96ab856'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.create_table('dep_accounts',\n        sa.Column('id', sa.Integer(), nullable=False),\n        sa.Column('certificate_id', sa.Integer(), nullable=True),\n        sa.Column('consumer_key', sa.String(), nullable=True),\n        sa.Column('consumer_secret', sa.String(), nullable=True),\n        sa.Column('access_token', sa.String(), nullable=True),\n        sa.Column('access_secret', sa.String(), nullable=True),\n        sa.Column('access_token_expiry', sa.DateTime(), nullable=True),\n        sa.Column('token_updated_at', sa.DateTime(), nullable=True),\n        sa.Column('auth_session_token', sa.String(), nullable=True),\n        sa.Column('server_name', sa.String(), nullable=True),\n        sa.Column('server_uuid', commandment.dbtypes.GUID(), nullable=True),\n        sa.Column('admin_id', sa.String(), nullable=True),\n        sa.Column('facilitator_id', sa.String(), nullable=True),\n        sa.Column('org_name', sa.String(), nullable=True),\n        sa.Column('org_email', sa.String(), nullable=True),\n        sa.Column('org_phone', sa.String(), nullable=True),\n        sa.Column('org_address', sa.String(), nullable=True),\n        sa.Column('org_type', sa.Enum('Education', 'Organization', name='deporgtype'), nullable=True),\n        sa.Column('org_version', sa.Enum('v1', 'v2', name='deporgversion'), nullable=True),\n        sa.Column('org_id', sa.String(), nullable=True),\n        sa.Column('org_id_hash', sa.String(), nullable=True),\n        sa.Column('url', sa.String(), nullable=True),\n        sa.Column('cursor', sa.String(), nullable=True),\n        sa.Column('more_to_follow', sa.Boolean(), nullable=True),\n        sa.Column('fetched_until', sa.DateTime(), nullable=True),\n        sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),\n        sa.PrimaryKeyConstraint('id')\n    )\n\n\ndef schema_downgrades():\n    op.drop_table('dep_accounts')\n\n\n\n# def data_upgrades():\n#     \"\"\"Add any optional data upgrade migrations here!\"\"\"\n#     pass\n#\n#\n# def data_downgrades():\n#     \"\"\"Add any optional data downgrade migrations here!\"\"\"\n#     pass\n"
  },
  {
    "path": "commandment/alembic/versions/2f1507bf6dc1_create_application_manifests_table.py",
    "content": "\"\"\"create application_manifests table\n\nRevision ID: 2f1507bf6dc1\nRevises: 7ab500f58a76\nCreate Date: 2017-10-15 17:37:04.645717\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '2f1507bf6dc1'\ndown_revision = '7ab500f58a76'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.create_table(\n        'application_manifests',\n        sa.Column('id', sa.Integer(), primary_key=True),\n        sa.Column('bundle_id', sa.String(), nullable=False),\n        sa.Column('bundle_version', sa.String()),\n        sa.Column('kind', sa.String()),\n        sa.Column('size_in_bytes', sa.BigInteger()),\n        sa.Column('subtitle', sa.String()),\n        sa.Column('title', sa.String()),\n        sa.Column('display_image_url', sa.String()),\n        sa.Column('display_image_needs_shine', sa.Boolean()),\n        sa.Column('full_size_image_url', sa.String()),\n        sa.Column('full_size_image_needs_shine', sa.Boolean()),\n\n        #op.add_column('application_manifests', sa.Column('full_size_image_needs_shine', sa.Boolean(), nullable=True))\n        #op.add_column('application_manifests', sa.Column('full_size_image_url', sa.String(), nullable=True))\n        # sa.UniqueConstraint('bundle_id', 'bundle_version', name='uq_application_bundle_version')\n    )\n\n    op.create_table(\n        'application_manifest_checksums',\n        sa.Column('id', sa.Integer(), primary_key=True),\n        sa.Column('application_manifest_id', sa.Integer(), nullable=True),\n        sa.Column('checksum_index', sa.Integer(), nullable=False),\n        sa.Column('checksum_value', sa.String(), nullable=False),\n        sa.ForeignKeyConstraint(['application_manifest_id'], ['application_manifests.id']),\n        # sa.ForeignKeyConstraint(['application_manifest_id'], ['application_manifests.id'], ondelete=\"CASCADE\"),\n        # sa.UniqueConstraint('application_manifest_id', 'checksum_index', name='uq_application_checksum_index')\n    )\n\n    # Commented items from an earlier migration:\n    # op.create_table('applications_manifests',\n    #                 sa.Column('id', sa.Integer(), nullable=False),\n    #                 sa.Column('bundle_id', sa.String(), nullable=False),\n    #                 sa.Column('bundle_version', sa.String(), nullable=True),\n    #                 sa.Column('kind', sa.String(), nullable=True),\n    #                 sa.Column('size_in_bytes', sa.BigInteger(), nullable=True),\n    #                 sa.Column('subtitle', sa.String(), nullable=True),\n    #                 sa.Column('title', sa.String(), nullable=True),\n    #                 sa.PrimaryKeyConstraint('id')\n    #                 )\n    # op.create_index(op.f('ix_applications_manifests_bundle_id'), 'applications_manifests', ['bundle_id'], unique=False)\n    # op.create_index(op.f('ix_applications_manifests_bundle_version'), 'applications_manifests', ['bundle_version'],\n    #                 unique=False)\n    # op.create_table('application_manifest_checksums',\n    #                 sa.Column('id', sa.Integer(), nullable=False),\n    #                 sa.Column('application_manifest_id', sa.Integer(), nullable=True),\n    #                 sa.Column('checksum_index', sa.Integer(), nullable=False),\n    #                 sa.Column('checksum_value', sa.String(), nullable=False),\n    #                 sa.ForeignKeyConstraint(['application_manifest_id'], ['applications_manifests.id'], ),\n    #                 sa.PrimaryKeyConstraint('id')\n    #                 )\n    # op.create_unique_constraint(\n    #     op.f('uq_application_manifest_checksum_manifest_index'),\n    #     'application_manifest_checksums', ['application_manifest_id', 'checksum_index'])\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n\n    # Commented items from an earlier migration:\n    # op.drop_constraint(op.f('uq_application_manifest_checksum_manifest_index'),\n    #                    table_name='application_manifest_checksums')\n    # op.drop_table('application_manifest_checksums')\n    # op.drop_index(op.f('ix_applications_manifests_bundle_version'), table_name='applications_manifests')\n    # op.drop_index(op.f('ix_applications_manifests_bundle_id'), table_name='applications_manifests')\n    # op.drop_table('applications_manifests')\n\n    op.drop_table('application_manifest_checksums')\n    op.drop_table('application_manifests')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/3061e56045eb_create_certificate_authority.py",
    "content": "\"\"\"create certificate authority\n\nRevision ID: 3061e56045eb\nRevises: 3fb4a904979c\nCreate Date: 2018-06-30 20:53:58.016051\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '3061e56045eb'\ndown_revision = '3fb4a904979c'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.create_table('certificate_authority',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('common_name', sa.String(), nullable=True),\n\n    # NOTE: certificate serials are still string but this remains as BigInteger because the counter is incremented\n    # manually.\n    sa.Column('serial', sa.BigInteger(), nullable=True),\n    sa.Column('validity_period', sa.Integer(), nullable=True),\n    sa.Column('certificate_id', sa.Integer(), nullable=True),\n    sa.Column('rsa_private_key_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),\n    sa.ForeignKeyConstraint(['rsa_private_key_id'], ['rsa_private_keys.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('common_name')\n    )\n\n\ndef schema_downgrades():\n    op.drop_table('certificate_authority')\n\n\n# def data_upgrades():\n#     \"\"\"Add any optional data upgrade migrations here!\"\"\"\n#     pass\n#\n#\n# def data_downgrades():\n#     \"\"\"Add any optional data downgrade migrations here!\"\"\"\n#     pass\n"
  },
  {
    "path": "commandment/alembic/versions/3dbf6db7f9eb_application_tags.py",
    "content": "\"\"\"application_tags\n\nRevision ID: 3dbf6db7f9eb\nRevises: 7cf5787a089e\nCreate Date: 2019-01-08 20:51:11.845673\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '3dbf6db7f9eb'\ndown_revision = '7cf5787a089e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('application_tags',\n    sa.Column('application_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['application_id'], ['applications.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], )\n    )\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_table('application_tags')\n"
  },
  {
    "path": "commandment/alembic/versions/3fb4a904979c_general_cleanup.py",
    "content": "\"\"\"general cleanup\n\nRevision ID: 3fb4a904979c\nRevises: 1532dff16984\nCreate Date: 2018-03-13 21:27:55.983564\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '3fb4a904979c'\ndown_revision = '1532dff16984'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.drop_table('users')\n    op.drop_table('payload_dependencies')\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.create_table('payload_dependencies',\n                    sa.Column('payload_uuid', sa.CHAR(length=32), nullable=True),\n                    sa.Column('depends_on_payload_uuid', sa.CHAR(length=32), nullable=True),\n                    sa.ForeignKeyConstraint(['depends_on_payload_uuid'], ['payloads.uuid'], ),\n                    sa.ForeignKeyConstraint(['payload_uuid'], ['payloads.uuid'], )\n                    )\n    op.create_table('users',\n                    sa.Column('id', sa.INTEGER(), nullable=False),\n                    sa.Column('name', sa.VARCHAR(), nullable=True),\n                    sa.Column('fullname', sa.VARCHAR(), nullable=True),\n                    sa.Column('password', sa.VARCHAR(), nullable=True),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.drop_table('dep_configurations')\n    op.drop_table('mdm_payload')\n    op.drop_table('certificate_payload')\n    op.drop_table('command_sequences')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/50188ffaf0cd_create_devices_table.py",
    "content": "\"\"\"Create devices table\n\nRevision ID: 50188ffaf0cd\nRevises: 71ecf957301a\nCreate Date: 2017-05-19 19:39:22.021264\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '50188ffaf0cd'\ndown_revision = '71ecf957301a'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('devices',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('udid', sa.String(), nullable=True),\n                    sa.Column('topic', sa.String(), nullable=True),\n                    sa.Column('last_seen', sa.DateTime(), nullable=True),\n                    sa.Column('is_enrolled', sa.Boolean(), nullable=True),\n                    sa.Column('build_version', sa.String(), nullable=True),\n                    sa.Column('device_name', sa.String(), nullable=True),\n                    sa.Column('model', sa.String(), nullable=True),\n                    sa.Column('model_name', sa.String(), nullable=True),\n                    sa.Column('os_version', sa.String(), nullable=True),\n                    sa.Column('product_name', sa.String(), nullable=True),\n                    sa.Column('serial_number', sa.String(length=64), nullable=True),\n                    sa.Column('hostname', sa.String(), nullable=True),\n                    sa.Column('local_hostname', sa.String(), nullable=True),\n                    sa.Column('available_device_capacity', sa.Float(), nullable=True),\n                    sa.Column('device_capacity', sa.Float(), nullable=True),\n                    sa.Column('wifi_mac', sa.String(), nullable=True),\n                    sa.Column('bluetooth_mac', sa.String(), nullable=True),\n                    sa.Column('awaiting_configuration', sa.Boolean(), nullable=True),\n                    sa.Column('push_magic', sa.String(), nullable=True),\n                    sa.Column('_token', sa.String(), nullable=True),\n                    sa.Column('tokenupdate_at', sa.DateTime(), nullable=True),\n                    sa.Column('last_push_at', sa.DateTime(), nullable=True),\n                    sa.Column('last_apns_id', sa.Integer(), nullable=True),\n                    sa.Column('failed_push_count', sa.Integer(), nullable=False),\n                    sa.Column('unlock_token', sa.String(), nullable=True),\n                    sa.Column('passcode_present', sa.Boolean(), nullable=True),\n                    sa.Column('passcode_compliant', sa.Boolean(), nullable=True),\n                    sa.Column('passcode_compliant_with_profiles', sa.Boolean(), nullable=True),\n                    sa.Column('fde_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('fde_has_prk', sa.Boolean(), nullable=True),\n                    sa.Column('fde_has_irk', sa.Boolean(), nullable=True),\n                    sa.Column('fde_personal_recovery_key_cms', sa.LargeBinary(), nullable=True),\n                    sa.Column('fde_personal_recovery_key_device_key', sa.String(), nullable=True),\n                    sa.Column('firewall_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('block_all_incoming', sa.Boolean(), nullable=True),\n                    sa.Column('stealth_mode_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('sip_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('certificate_id', sa.Integer(), nullable=True),\n                    sa.Column('battery_level', sa.Float(), nullable=True),\n                    sa.Column('carrier_settings_version', sa.String(), nullable=True),\n                    sa.Column('cellular_technology', sa.Enum('Nothing', 'GSM', 'CDMA', 'Both', name='cellulartechnology'), nullable=True),\n                    sa.Column('current_carrier_network', sa.String(), nullable=True),\n                    sa.Column('current_mcc', sa.String(), nullable=True),\n                    sa.Column('current_mnc', sa.String(), nullable=True),\n                    sa.Column('data_roaming_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('device_id', sa.String(), nullable=True),\n                    sa.Column('eas_device_identifier', sa.String(), nullable=True),\n                    sa.Column('iccid', sa.String(), nullable=True),\n                    sa.Column('imei', sa.String(), nullable=True),\n                    sa.Column('is_activation_lock_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('is_cloud_backup_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('is_device_locator_service_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('is_do_not_disturb_in_effect', sa.Boolean(), nullable=True),\n                    sa.Column('is_mdm_lost_mode_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('is_roaming', sa.Boolean(), nullable=True),\n                    sa.Column('is_supervised', sa.Boolean(), nullable=True),\n                    sa.Column('itunes_store_account_hash', sa.String(), nullable=True),\n                    sa.Column('itunes_store_account_is_active', sa.Boolean(), nullable=True),\n                    sa.Column('last_cloud_backup_date', sa.DateTime(), nullable=True),\n                    sa.Column('maximum_resident_users', sa.Integer(), nullable=True),\n                    sa.Column('meid', sa.String(), nullable=True),\n                    sa.Column('modem_firmware_version', sa.String(), nullable=True),\n                    sa.Column('passcode_lock_grace_period_enforced', sa.Integer(), nullable=True),\n                    sa.Column('personal_hotspot_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('phone_number', sa.String(), nullable=True),\n                    sa.Column('sim_carrier_network', sa.String(), nullable=True),\n                    sa.Column('subscriber_carrier_network', sa.String(), nullable=True),\n                    sa.Column('subscriber_mcc', sa.String(), nullable=True),\n                    sa.Column('subscriber_mnc', sa.String(), nullable=True),\n                    sa.Column('voice_roaming_enabled', sa.Boolean(), nullable=True),\n                    sa.Column('activation_lock_escrow_key', sa.String(), nullable=True),\n                    sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_devices_serial_number'), 'devices', ['serial_number'], unique=False)\n    op.create_index(op.f('ix_devices_udid'), 'devices', ['udid'], unique=False)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_devices_udid'), table_name='devices')\n    op.drop_index(op.f('ix_devices_serial_number'), table_name='devices')\n    op.drop_table('devices')\n"
  },
  {
    "path": "commandment/alembic/versions/5b98cc4af6c9_create_profiles_table.py",
    "content": "\"\"\"Create profiles table\n\nRevision ID: 5b98cc4af6c9\nRevises: e78274be170e\nCreate Date: 2017-05-19 19:30:47.058720\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = '5b98cc4af6c9'\ndown_revision = 'e78274be170e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('profiles',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('data', sa.LargeBinary(), nullable=True),\n                    sa.Column('payload_type', sa.String(), nullable=True),\n                    sa.Column('description', sa.Text(), nullable=True),\n                    sa.Column('display_name', sa.String(), nullable=True),\n                    sa.Column('expiration_date', sa.DateTime(), nullable=True),\n                    sa.Column('identifier', sa.String(), nullable=False),\n                    sa.Column('organization', sa.String(), nullable=True),\n                    sa.Column('uuid', commandment.dbtypes.GUID(), nullable=True),\n                    sa.Column('removal_disallowed', sa.Boolean(), nullable=True),\n                    sa.Column('version', sa.Integer(), nullable=True),\n                    sa.Column('scope', sa.Enum('User', 'System', name='payloadscope'), nullable=True),\n                    sa.Column('removal_date', sa.DateTime(), nullable=True),\n                    sa.Column('duration_until_removal', sa.BigInteger(), nullable=True),\n                    sa.Column('consent_en', sa.Text(), nullable=True),\n                    sa.Column('is_encrypted', sa.Boolean(), nullable=True),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_profiles_uuid'), 'profiles', ['uuid'], unique=True)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_profiles_uuid'), table_name='profiles')\n    op.drop_table('profiles')\n"
  },
  {
    "path": "commandment/alembic/versions/6675e981817e_create_available_os_updates_table.py",
    "content": "\"\"\"create available_os_updates table\n\nRevision ID: 6675e981817e\nRevises: 70ff84113e8f\nCreate Date: 2017-06-23 17:40:11.879267\n\n\"\"\"\nfrom alembic import op, context\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = '6675e981817e'\ndown_revision = '70ff84113e8f'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('available_os_updates',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('device_id', sa.Integer(), nullable=True),\n    sa.Column('allows_install_later', sa.Boolean(), nullable=True),\n    sa.Column('app_identifiers_to_close', commandment.dbtypes.JSONEncodedDict(), nullable=True),\n    sa.Column('human_readable_name', sa.String(), nullable=True),\n    sa.Column('human_readable_name_locale', sa.String(), nullable=True),\n    sa.Column('is_config_data_update', sa.Boolean(), nullable=True),\n    sa.Column('is_critical', sa.Boolean(), nullable=True),\n    sa.Column('is_firmware_update', sa.Boolean(), nullable=True),\n    sa.Column('metadata_url', sa.String(), nullable=True),\n    sa.Column('product_key', sa.String(), nullable=True),\n    sa.Column('restart_required', sa.Boolean(), nullable=True),\n    sa.Column('version', sa.String(), nullable=True),\n    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_table('available_os_updates')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/70ff84113e8f_create_tags.py",
    "content": "\"\"\"Create tags table and join tables\n\nRevision ID: 70ff84113e8f\nRevises: 7ae48ae412d7\nCreate Date: 2017-06-20 17:13:11.572353\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '70ff84113e8f'\ndown_revision = 'dd74229d17b9'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('tags',\n                    sa.Column('id', sa.Integer(), nullable=False, autoincrement=True),\n                    sa.Column('name', sa.String(), nullable=False),\n                    sa.Column('color', sa.String(length=6), nullable=True),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_table('profile_tags',\n                    sa.Column('profile_id', sa.Integer(), nullable=True),\n                    sa.Column('tag_id', sa.Integer(), nullable=True),\n                    sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ondelete=\"CASCADE\"),\n                    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ondelete=\"CASCADE\")\n                    )\n    op.create_index(op.f('ix_profile_tags'), 'profile_tags', ['profile_id', 'tag_id'], unique=True)\n    op.create_table('device_tags',\n                    sa.Column('device_id', sa.Integer(), nullable=True),\n                    sa.Column('tag_id', sa.Integer(), nullable=True),\n                    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ondelete=\"CASCADE\"),\n                    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ondelete=\"CASCADE\")\n                    )\n    op.create_index(op.f('ix_device_tags'), 'device_tags', ['device_id', 'tag_id'], unique=True)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_device_tags'), table_name='device_tags')\n    op.drop_table('device_tags')\n    op.drop_index(op.f('ix_profile_tags'), table_name='profile_tags')\n    op.drop_table('profile_tags')\n    op.drop_table('tags')\n"
  },
  {
    "path": "commandment/alembic/versions/71818e983100_create_application_sources_table.py",
    "content": "\"\"\"Create application_sources table\n\nRevision ID: 71818e983100\nRevises: da52b64b865f\nCreate Date: 2017-05-18 22:29:40.036227\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '71818e983100'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('application_sources',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('name', sa.String(), nullable=True),\n                    sa.Column('source_type', sa.Enum('S3', 'Munki', name='appsourcetype'), nullable=True),\n                    sa.Column('endpoint', sa.String(), nullable=True),\n                    sa.Column('mount_uri', sa.String(), nullable=True),\n                    sa.Column('use_ssl', sa.Boolean(), nullable=True),\n                    sa.Column('access_key', sa.String(), nullable=True),\n                    sa.Column('secret_key', sa.String(), nullable=True),\n                    sa.Column('bucket', sa.String(), nullable=True),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('application_sources')\n"
  },
  {
    "path": "commandment/alembic/versions/71ecf957301a_create_commands_table.py",
    "content": "\"\"\"Create commands table\n\nRevision ID: 71ecf957301a\nRevises: af4ba256efde\nCreate Date: 2017-05-19 19:38:21.450906\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = '71ecf957301a'\ndown_revision = 'af4ba256efde'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('commands',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('request_type', sa.String(), nullable=False),\n                    sa.Column('uuid', commandment.dbtypes.GUID(), nullable=False),\n                    sa.Column('parameters', commandment.dbtypes.JSONEncodedDict(), nullable=True),\n                    sa.Column('status', sa.String(length=40), nullable=False),\n                    sa.Column('queued_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),\n                    sa.Column('sent_at', sa.DateTime(), nullable=True),\n                    sa.Column('acknowledged_at', sa.DateTime(), nullable=True),\n                    sa.Column('after', sa.DateTime(), nullable=True),\n                    sa.Column('ttl', sa.Integer(), nullable=False),\n                    sa.Column('device_id', sa.Integer(), nullable=True),\n                    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_commands_status'), 'commands', ['status'], unique=False)\n    op.create_index(op.f('ix_commands_uuid'), 'commands', ['uuid'], unique=True)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_commands_uuid'), table_name='commands')\n    op.drop_index(op.f('ix_commands_status'), table_name='commands')\n    op.drop_table('commands')\n"
  },
  {
    "path": "commandment/alembic/versions/7ab500f58a76_create_installed_payloads.py",
    "content": "\"\"\"create installed_payloads\n\nRevision ID: 7ab500f58a76\nRevises: f029ac1af3f0\nCreate Date: 2017-07-19 14:17:49.094292\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '7ab500f58a76'\ndown_revision = 'f029ac1af3f0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.create_table('installed_payloads',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('profile_id', sa.Integer(), nullable=False),\n    sa.Column('device_id', sa.Integer(), nullable=False),\n    sa.Column('description', sa.String(), nullable=True),\n    sa.Column('display_name', sa.String(), nullable=True),\n    sa.Column('identifier', sa.String(), nullable=True),\n    sa.Column('organization', sa.String(), nullable=True),\n    sa.Column('payload_type', sa.String(), nullable=True),\n    sa.Column('uuid', commandment.dbtypes.GUID(), nullable=True),\n    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ondelete=\"CASCADE\"),\n    sa.ForeignKeyConstraint(['profile_id'], ['installed_profiles.id'], ondelete=\"CASCADE\"),\n    sa.PrimaryKeyConstraint('id')\n    )\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_table('installed_payloads')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/7cf5787a089e_add_dep_profile_relationships.py",
    "content": "\"\"\"add dep profile relationships\n\nRevision ID: 7cf5787a089e\nRevises: b231394ab475\nCreate Date: 2018-11-06 21:11:54.606189\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '7cf5787a089e'\ndown_revision = 'b231394ab475'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    with op.batch_alter_table('dep_accounts', schema=None) as batch_op:\n        batch_op.add_column(sa.Column('default_dep_profile_id', sa.Integer(), nullable=True))\n        batch_op.create_foreign_key('fk_dep_accounts_default_dep_profile_id', 'dep_profiles', ['default_dep_profile_id'], ['id'])\n\n    with op.batch_alter_table('dep_profiles', schema=None) as batch_op:\n        batch_op.add_column(sa.Column('dep_account_id', sa.Integer(), nullable=True))\n        batch_op.add_column(sa.Column('skip_setup_items', commandment.dbtypes.JSONEncodedDict(), nullable=True))\n        batch_op.create_foreign_key('fk_dep_profiles_dep_account_id', 'dep_accounts', ['dep_account_id'], ['id'])\n\n\ndef schema_downgrades():\n    with op.batch_alter_table('dep_profiles', schema=None) as batch_op:\n        batch_op.drop_constraint('fk_dep_profiles_dep_account_id', 'dep_profiles', type_='foreignkey')\n        batch_op.drop_column('skip_setup_items')\n        batch_op.drop_column('dep_account_id')\n\n    with op.batch_alter_table('dep_accounts', schema=None) as batch_op:\n        batch_op.drop_constraint('fk_dep_accounts_default_dep_profile_id', 'dep_accounts', type_='foreignkey')\n        batch_op.drop_column('default_dep_profile_id')\n"
  },
  {
    "path": "commandment/alembic/versions/7d578eb75092_create_device_groups_table.py",
    "content": "\"\"\"Create device_groups table\n\nRevision ID: 7d578eb75092\nRevises: 71818e983100\nCreate Date: 2017-05-18 22:31:16.686848\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '7d578eb75092'\ndown_revision = '71818e983100'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('device_groups',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('name', sa.String(), nullable=False),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('device_groups')\n"
  },
  {
    "path": "commandment/alembic/versions/80fa1767c7e2_create_oauth_server_models.py",
    "content": "\"\"\"Create OAuth Server Models\n\nRevision ID: 80fa1767c7e2\nRevises: fa4d91c6aacf\nCreate Date: 2019-05-20 20:47:04.928849\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '80fa1767c7e2'\ndown_revision = 'fa4d91c6aacf'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n\n\ndef downgrade():\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.create_table('oauth2_clients',\n    sa.Column('client_id', sa.String(length=48), nullable=True),\n    sa.Column('client_secret', sa.String(length=120), nullable=True),\n    sa.Column('issued_at', sa.Integer(), nullable=False),\n    sa.Column('expires_at', sa.Integer(), nullable=False),\n    sa.Column('redirect_uri', sa.Text(), nullable=True),\n    sa.Column('token_endpoint_auth_method', sa.String(length=48), nullable=True),\n    sa.Column('grant_type', sa.Text(), nullable=False),\n    sa.Column('response_type', sa.Text(), nullable=False),\n    sa.Column('scope', sa.Text(), nullable=False),\n    sa.Column('client_name', sa.String(length=100), nullable=True),\n    sa.Column('client_uri', sa.Text(), nullable=True),\n    sa.Column('logo_uri', sa.Text(), nullable=True),\n    sa.Column('contact', sa.Text(), nullable=True),\n    sa.Column('tos_uri', sa.Text(), nullable=True),\n    sa.Column('policy_uri', sa.Text(), nullable=True),\n    sa.Column('jwks_uri', sa.Text(), nullable=True),\n    sa.Column('jwks_text', sa.Text(), nullable=True),\n    sa.Column('i18n_metadata', sa.Text(), nullable=True),\n    sa.Column('software_id', sa.String(length=36), nullable=True),\n    sa.Column('software_version', sa.String(length=48), nullable=True),\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('user_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),\n    sa.PrimaryKeyConstraint('id')\n    )\n    with op.batch_alter_table('oauth2_clients', schema=None) as batch_op:\n        batch_op.create_index(batch_op.f('ix_oauth2_clients_client_id'), ['client_id'], unique=False)\n\n    op.create_table('oauth2_tokens',\n    sa.Column('client_id', sa.String(length=48), nullable=True),\n    sa.Column('token_type', sa.String(length=40), nullable=True),\n    sa.Column('access_token', sa.String(length=255), nullable=False),\n    sa.Column('refresh_token', sa.String(length=255), nullable=True),\n    sa.Column('scope', sa.Text(), nullable=True),\n    sa.Column('revoked', sa.Boolean(), nullable=True),\n    sa.Column('issued_at', sa.Integer(), nullable=False),\n    sa.Column('expires_in', sa.Integer(), nullable=False),\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('user_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('access_token')\n    )\n    with op.batch_alter_table('oauth2_tokens', schema=None) as batch_op:\n        batch_op.create_index(batch_op.f('ix_oauth2_tokens_refresh_token'), ['refresh_token'], unique=False)\n\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(), nullable=True),\n    sa.Column('fullname', sa.String(), nullable=True),\n    sa.Column('password', sa.String(), nullable=True),\n    sa.PrimaryKeyConstraint('id')\n    )\n\n\ndef schema_downgrades():\n    op.drop_table('users')\n    with op.batch_alter_table('oauth2_tokens', schema=None) as batch_op:\n        batch_op.drop_index(batch_op.f('ix_oauth2_tokens_refresh_token'))\n\n    op.drop_table('oauth2_tokens')\n    with op.batch_alter_table('oauth2_clients', schema=None) as batch_op:\n        batch_op.drop_index(batch_op.f('ix_oauth2_clients_client_id'))\n\n    op.drop_table('oauth2_clients')\n"
  },
  {
    "path": "commandment/alembic/versions/875dcce0bf8b_create_vpp_users.py",
    "content": "\"\"\"Create vpp_users table\n\nRevision ID: 875dcce0bf8b\nRevises: a2e0af380181\nCreate Date: 2017-07-19 12:56:02.203987\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '875dcce0bf8b'\ndown_revision = 'a2e0af380181'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.create_table('vpp_users',\n    sa.Column('user_id', sa.Integer(), nullable=False),\n    sa.Column('client_user_id', commandment.dbtypes.GUID(), nullable=False),\n    sa.Column('email', sa.String(), nullable=True),\n    sa.Column('status', sa.Enum('Registered', 'Associated', 'Retired', 'Deleted', name='vppuserstatus'), nullable=True),\n    sa.Column('invite_url', sa.String(), nullable=True),\n    sa.Column('invite_code', sa.String(), nullable=True),\n    sa.PrimaryKeyConstraint('user_id')\n    )\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_table('vpp_users')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/8c866896f76e_create_dep_join_tables.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8c866896f76e\nRevises: 0e5babc5b9ee\nCreate Date: 2017-07-19 12:57:58.086196\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = '8c866896f76e'\ndown_revision = '0e5babc5b9ee'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.create_table('dep_profile_anchor_certificates',\n    sa.Column('dep_profile_id', sa.Integer(), nullable=True),\n    sa.Column('certificate_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),\n    sa.ForeignKeyConstraint(['dep_profile_id'], ['dep_profiles.id'], )\n    )\n    op.create_table('dep_profile_supervision_certificates',\n    sa.Column('dep_profile_id', sa.Integer(), nullable=True),\n    sa.Column('certificate_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),\n    sa.ForeignKeyConstraint(['dep_profile_id'], ['dep_profiles.id'], )\n    )\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_table('dep_profile_supervision_certificates')\n    op.drop_table('dep_profile_anchor_certificates')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/__init__.py",
    "content": ""
  },
  {
    "path": "commandment/alembic/versions/a1d5ffaa2092_create_installed_applications_table.py",
    "content": "\"\"\"Create installed_applications table\n\nRevision ID: a1d5ffaa2092\nRevises: a35eeb5a216e\nCreate Date: 2017-05-19 19:43:10.092363\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = 'a1d5ffaa2092'\ndown_revision = 'a35eeb5a216e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('installed_applications',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('device_udid', sa.String(40), nullable=False),\n                    sa.Column('device_id', sa.Integer(), nullable=True),\n                    sa.Column('bundle_identifier', sa.String(), nullable=True),\n                    sa.Column('version', sa.String(), nullable=True),\n                    sa.Column('short_version', sa.String(), nullable=True),\n                    sa.Column('name', sa.String(), nullable=True),\n                    sa.Column('bundle_size', sa.BigInteger(), nullable=True),\n                    sa.Column('dynamic_size', sa.BigInteger(), nullable=True),\n                    sa.Column('is_validated', sa.Boolean(), nullable=True),\n                    sa.Column('external_version_identifier', sa.BigInteger(), nullable=True),\n                    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_installed_applications_bundle_identifier'), 'installed_applications', ['bundle_identifier'], unique=False)\n    op.create_index(op.f('ix_installed_applications_device_udid'), 'installed_applications', ['device_udid'], unique=False)\n    op.create_index(op.f('ix_installed_applications_version'), 'installed_applications', ['version'], unique=False)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_installed_applications_version'), table_name='installed_applications')\n    op.drop_index(op.f('ix_installed_applications_device_udid'), table_name='installed_applications')\n    op.drop_index(op.f('ix_installed_applications_bundle_identifier'), table_name='installed_applications')\n    op.drop_table('installed_applications')\n"
  },
  {
    "path": "commandment/alembic/versions/a2e0af380181_create_dep_profiles.py",
    "content": "\"\"\"Create dep_profiles table\n\nRevision ID: a2e0af380181\nRevises: 6675e981817e\nCreate Date: 2017-07-19 12:50:41.318647\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'a2e0af380181'\ndown_revision = '6675e981817e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.create_table('dep_profiles',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('uuid', commandment.dbtypes.GUID(), nullable=True),\n    sa.Column('profile_name', sa.String(), nullable=False),\n    sa.Column('url', sa.String(), nullable=False),\n    sa.Column('allow_pairing', sa.Boolean(), nullable=True),\n    sa.Column('is_supervised', sa.Boolean(), nullable=True),\n    sa.Column('is_multi_user', sa.Boolean(), nullable=True),\n    sa.Column('is_mandatory', sa.Boolean(), nullable=True),\n    sa.Column('await_device_configured', sa.Boolean(), nullable=True),\n    sa.Column('is_mdm_removable', sa.Boolean(), nullable=True),\n    sa.Column('support_phone_number', sa.String(), nullable=True),\n    sa.Column('auto_advance_setup', sa.Boolean(), nullable=True),\n    sa.Column('support_email_address', sa.String(), nullable=True),\n    sa.Column('org_magic', sa.String(), nullable=True),\n    sa.Column('department', sa.String(), nullable=True),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_index(op.f('ix_dep_profiles_uuid'), 'dep_profiles', ['uuid'], unique=False)\n    # op.create_foreign_key(None, 'devices', 'dep_profiles', ['dep_profile_id'], ['id'])\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_index(op.f('ix_dep_profiles_uuid'), table_name='dep_profiles')\n    op.drop_table('dep_profiles')\n    #     op.drop_constraint(None, 'devices', type_='foreignkey')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/a35eeb5a216e_create_installed_profiles_table.py",
    "content": "\"\"\"Create installed_profiles table\n\nRevision ID: a35eeb5a216e\nRevises: e16577adc4fd\nCreate Date: 2017-05-19 19:41:46.995463\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = 'a35eeb5a216e'\ndown_revision = 'e16577adc4fd'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('installed_profiles',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('device_udid', sa.String(40), nullable=False),\n                    sa.Column('device_id', sa.Integer(), nullable=True),\n                    sa.Column('has_removal_password', sa.Boolean(), nullable=True),\n                    sa.Column('is_encrypted', sa.Boolean(), nullable=True),\n                    sa.Column('is_managed', sa.Boolean(), nullable=True),\n                    sa.Column('payload_description', sa.String(), nullable=True),\n                    sa.Column('payload_display_name', sa.String(), nullable=True),\n                    sa.Column('payload_identifier', sa.String(), nullable=True),\n                    sa.Column('payload_organization', sa.String(), nullable=True),\n                    sa.Column('payload_removal_disallowed', sa.Boolean(), nullable=True),\n                    sa.Column('payload_uuid', commandment.dbtypes.GUID(), nullable=True),\n                    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_installed_profiles_device_udid'), 'installed_profiles', ['device_udid'], unique=False)\n    op.create_index(op.f('ix_installed_profiles_payload_uuid'), 'installed_profiles', ['payload_uuid'], unique=False)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_installed_profiles_payload_uuid'), table_name='installed_profiles')\n    op.drop_index(op.f('ix_installed_profiles_device_udid'), table_name='installed_profiles')\n    op.drop_table('installed_profiles')\n"
  },
  {
    "path": "commandment/alembic/versions/a3ddaad5c358_add_dep_device_columns.py",
    "content": "\"\"\"Add DEP device columns\n\nRevision ID: a3ddaad5c358\nRevises: 2808deb9fc62\nCreate Date: 2018-07-04 21:44:41.549806\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'a3ddaad5c358'\ndown_revision = '2808deb9fc62'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.add_column('devices', sa.Column('description', sa.String(), nullable=True))\n    op.add_column('devices', sa.Column('asset_tag', sa.String(), nullable=True))\n    op.add_column('devices', sa.Column('color', sa.String(), nullable=True))\n    op.add_column('devices', sa.Column('device_assigned_by', sa.String(), nullable=True))\n    op.add_column('devices', sa.Column('device_assigned_date', sa.DateTime(), nullable=True))\n    op.add_column('devices', sa.Column('device_family', sa.String(), nullable=True))\n    op.add_column('devices', sa.Column('is_dep', sa.Boolean(), nullable=True))\n    op.add_column('devices', sa.Column('os', sa.String(), nullable=True))\n    op.add_column('devices', sa.Column('profile_assign_time', sa.DateTime(), nullable=True))\n    op.add_column('devices', sa.Column('profile_push_time', sa.DateTime(), nullable=True))\n    op.add_column('devices', sa.Column('profile_status', sa.String(), nullable=True))\n    op.add_column('devices', sa.Column('profile_uuid', sa.String(), nullable=True))\n\n\ndef schema_downgrades():\n    op.drop_column('devices', 'profile_uuid')\n    op.drop_column('devices', 'profile_status')\n    op.drop_column('devices', 'profile_push_time')\n    op.drop_column('devices', 'profile_assign_time')\n    op.drop_column('devices', 'os')\n    op.drop_column('devices', 'is_dep')\n    op.drop_column('devices', 'device_family')\n    op.drop_column('devices', 'device_assigned_date')\n    op.drop_column('devices', 'device_assigned_by')\n    op.drop_column('devices', 'color')\n    op.drop_column('devices', 'asset_tag')\n    op.drop_column('devices', 'description')\n\n\n# def data_upgrades():\n#     \"\"\"Add any optional data upgrade migrations here!\"\"\"\n#     pass\n#\n#\n# def data_downgrades():\n#     \"\"\"Add any optional data downgrade migrations here!\"\"\"\n#     pass\n"
  },
  {
    "path": "commandment/alembic/versions/af4ba256efde_create_certificates_table.py",
    "content": "\"\"\"Create certificates table\n\nRevision ID: af4ba256efde\nRevises: 0ab46b2f6d8c\nCreate Date: 2017-05-19 19:36:12.171390\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'af4ba256efde'\ndown_revision = '0ab46b2f6d8c'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('certificates',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('pem_data', sa.Text(), nullable=False),\n                    sa.Column('rsa_private_key_id', sa.Integer(), nullable=True),\n                    sa.Column('x509_cn', sa.String(length=64), nullable=True),\n                    sa.Column('x509_ou', sa.String(length=32), nullable=True),\n                    sa.Column('x509_o', sa.String(length=64), nullable=True),\n                    sa.Column('x509_c', sa.String(length=2), nullable=True),\n                    sa.Column('x509_st', sa.String(length=128), nullable=True),\n                    sa.Column('not_before', sa.DateTime(), nullable=False),\n                    sa.Column('not_after', sa.DateTime(), nullable=False),\n                    # NOTE: serial was changed from BigInteger because cryptography could generate a serial number at\n                    # random that could produce an integer overflow.\n                    sa.Column('serial', sa.String(), nullable=True),\n                    sa.Column('fingerprint', sa.String(length=64), nullable=False),\n                    sa.Column('push_topic', sa.String(), nullable=True),\n                    sa.Column('discriminator', sa.String(length=20), nullable=True),\n                    sa.ForeignKeyConstraint(['rsa_private_key_id'], ['rsa_private_keys.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_certificates_fingerprint'), 'certificates', ['fingerprint'], unique=False)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_certificates_fingerprint'), table_name='certificates')\n    op.drop_table('certificates')\n"
  },
  {
    "path": "commandment/alembic/versions/b231394ab475_add_scep_config_source_types.py",
    "content": "\"\"\"add scep_config source types\n\nRevision ID: b231394ab475\nRevises: a3ddaad5c358\nCreate Date: 2018-09-07 07:50:10.467330\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'b231394ab475'\ndown_revision = 'a3ddaad5c358'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n\n\ndef downgrade():\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.add_column('scep_config', sa.Column('source_type', sa.Enum('InternalPKCS12', 'InternalSCEP', 'ExternalSCEP', name='deviceidentitysources'), nullable=True))\n\n\ndef schema_downgrades():\n    op.drop_column('scep_config', 'source_type')\n\n"
  },
  {
    "path": "commandment/alembic/versions/b74ca08cfd9a_create_applications_tables.py",
    "content": "\"\"\"create applications tables\n\nRevision ID: b74ca08cfd9a\nRevises: 2f1507bf6dc1\nCreate Date: 2017-10-19 21:26:19.927682\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'b74ca08cfd9a'\ndown_revision = '2f1507bf6dc1'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.create_table('applications',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('display_name', sa.String(), nullable=False),\n                    sa.Column('description', sa.String(), nullable=True),\n                    sa.Column('version', sa.String(), nullable=True),\n                    sa.Column('itunes_store_id', sa.Integer(), nullable=True),\n                    sa.Column('bundle_id', sa.String(), nullable=False),\n                    sa.Column('purchase_method', sa.Integer(), nullable=True),\n                    sa.Column('manifest_url', sa.String(), nullable=True),\n                    sa.Column('management_flags', sa.Integer(), nullable=True),\n                    sa.Column('change_management_state', sa.String(), nullable=True),\n                    sa.Column('discriminator', sa.String(length=20), nullable=True),\n\n                    sa.Column('country', sa.String(length=2), nullable=True),\n                    sa.Column('artist_id', sa.Integer(), nullable=True),\n                    sa.Column('artist_name', sa.String(), nullable=True),\n                    sa.Column('artist_view_url', sa.String(), nullable=True),\n                    sa.Column('artwork_url60', sa.String(), nullable=True),\n                    sa.Column('artwork_url100', sa.String(), nullable=True),\n                    sa.Column('artwork_url512', sa.String(), nullable=True),\n                    sa.Column('release_notes', sa.Text(), nullable=True),\n                    sa.Column('release_date', sa.DateTime(), nullable=True),\n                    sa.Column('minimum_os_version', sa.String(), nullable=True),\n                    sa.Column('file_size_bytes', sa.BigInteger(), nullable=True),\n\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_applications_bundle_id'), 'applications', ['bundle_id'], unique=False)\n    op.create_index(op.f('ix_applications_discriminator'), 'applications', ['discriminator'], unique=False)\n\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_index(op.f('ix_applications_discriminator'), table_name='applications')\n    op.drop_index(op.f('ix_applications_bundle_id'), table_name='applications')\n    op.drop_table('applications')\n    # ### end Alembic commands ###\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/ba4849d8c8ad_create_device_group_devices_table.py",
    "content": "\"\"\"Create device_group_devices table\n\nRevision ID: ba4849d8c8ad\nRevises: a1d5ffaa2092\nCreate Date: 2017-05-19 19:44:37.403554\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'ba4849d8c8ad'\ndown_revision = 'a1d5ffaa2092'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('device_group_devices',\n                    sa.Column('device_group_id', sa.Integer(), nullable=False),\n                    sa.Column('device_id', sa.Integer(), nullable=False),\n                    sa.ForeignKeyConstraint(['device_group_id'], ['device_groups.id'], ),\n                    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ),\n                    sa.PrimaryKeyConstraint('device_group_id', 'device_id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('device_group_devices')\n"
  },
  {
    "path": "commandment/alembic/versions/d5b32b5cc74e_add_dep_profile_id_to_device.py",
    "content": "\"\"\"add dep profile id to device\n\nRevision ID: d5b32b5cc74e\nRevises: 1005dc7dea01\nCreate Date: 2018-03-13 21:16:23.964086\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'd5b32b5cc74e'\ndown_revision = '1005dc7dea01'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.add_column('devices', sa.Column('dep_profile_id', sa.Integer(), nullable=True))\n    # Unsupported on SQLite3\n    # op.create_foreign_key(None, 'devices', 'dep_profiles', ['dep_profile_id'], ['id'])\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    # Unsupported on SQLite3\n    # op.drop_constraint(None, 'devices', type_='foreignkey')\n    op.drop_column('devices', 'dep_profile_id')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/dd74229d17b9_create_payload_dependencies_table.py",
    "content": "\"\"\"Create payload_dependencies table\n\nRevision ID: dd74229d17b9\nRevises: d65049bf4b91\nCreate Date: 2017-05-19 20:02:17.116286\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = 'dd74229d17b9'\ndown_revision = 'e5840df9a88a'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('payload_dependencies',\n                    sa.Column('payload_uuid', commandment.dbtypes.GUID(), nullable=True),\n                    sa.Column('depends_on_payload_uuid', commandment.dbtypes.GUID(), nullable=True),\n                    sa.ForeignKeyConstraint(['depends_on_payload_uuid'], ['payloads.uuid'], ),\n                    sa.ForeignKeyConstraint(['payload_uuid'], ['payloads.uuid'], )\n                    )\n\n\ndef downgrade():\n    op.drop_table('payload_dependencies')\n"
  },
  {
    "path": "commandment/alembic/versions/e16577adc4fd_create_installed_certificates_table.py",
    "content": "\"\"\"Create installed_certificates table\n\nRevision ID: e16577adc4fd\nRevises: 50188ffaf0cd\nCreate Date: 2017-05-19 19:40:56.436486\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = 'e16577adc4fd'\ndown_revision = '50188ffaf0cd'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('installed_certificates',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('device_udid', sa.String(40), nullable=False),\n                    sa.Column('device_id', sa.Integer(), nullable=True),\n                    sa.Column('x509_cn', sa.String(), nullable=True),\n                    sa.Column('is_identity', sa.Boolean(), nullable=True),\n                    sa.Column('der_data', sa.LargeBinary(), nullable=False),\n                    sa.Column('fingerprint_sha256', sa.String(length=64), nullable=False),\n                    sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_installed_certificates_device_udid'), 'installed_certificates', ['device_udid'], unique=False)\n    op.create_index(op.f('ix_installed_certificates_fingerprint_sha256'), 'installed_certificates', ['fingerprint_sha256'], unique=False)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_installed_certificates_fingerprint_sha256'), table_name='installed_certificates')\n    op.drop_index(op.f('ix_installed_certificates_device_udid'), table_name='installed_certificates')\n    op.drop_table('installed_certificates')\n\n"
  },
  {
    "path": "commandment/alembic/versions/e5840df9a88a_create_scep_payload_table.py",
    "content": "\"\"\"Create scep_payload table\n\nRevision ID: e5840df9a88a\nRevises: fc0c134cbb2e\nCreate Date: 2017-05-19 19:58:54.048729\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = 'e5840df9a88a'\ndown_revision = '13358fb3846b'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('scep_payload',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('url', sa.String(), nullable=False),\n                    sa.Column('name', sa.String(), nullable=True),\n                    sa.Column('subject', commandment.dbtypes.JSONEncodedDict(), nullable=False),\n                    sa.Column('challenge', sa.String(), nullable=True),\n                    sa.Column('key_size', sa.Integer(), nullable=False),\n                    sa.Column('ca_fingerprint', sa.LargeBinary(), nullable=True),\n                    sa.Column('key_type', sa.String(), nullable=False),\n                    sa.Column('key_usage', sa.Enum('Signing', 'Encryption', 'All', name='keyusage'), nullable=True),\n                    sa.Column('subject_alt_name', sa.String(), nullable=True),\n                    sa.Column('retries', sa.Integer(), nullable=False),\n                    sa.Column('retry_delay', sa.Integer(), nullable=False),\n                    sa.Column('certificate_renewal_time_interval', sa.Integer(), nullable=False),\n                    sa.ForeignKeyConstraint(['id'], ['payloads.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('scep_payload')\n"
  },
  {
    "path": "commandment/alembic/versions/e58afdc17baa_create_rsa_private_keys_table.py",
    "content": "\"\"\"Create rsa_private_keys table\n\nRevision ID: e58afdc17baa\nRevises: 5b98cc4af6c9\nCreate Date: 2017-05-19 19:32:28.454940\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'e58afdc17baa'\ndown_revision = '5b98cc4af6c9'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('rsa_private_keys',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('pem_data', sa.Text(), nullable=False),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('rsa_private_keys')\n"
  },
  {
    "path": "commandment/alembic/versions/e78274be170e_create_organizations_table.py",
    "content": "\"\"\"Create organizations table\n\nRevision ID: e78274be170e\nRevises: e9b0a4f7b595\nCreate Date: 2017-05-19 19:28:42.596244\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nfrom sqlalchemy.sql import table\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'e78274be170e'\ndown_revision = 'e9b0a4f7b595'\nbranch_labels = None\ndepends_on = None\n\nTABLE = (\n    'organizations',\n    sa.Column('id', sa.Integer(), nullable=False, autoincrement=True),\n    sa.Column('name', sa.String(), nullable=True),\n    sa.Column('payload_prefix', sa.String(), nullable=True),\n    sa.Column('x509_ou', sa.String(length=32), nullable=True),\n    sa.Column('x509_o', sa.String(length=64), nullable=True),\n    sa.Column('x509_st', sa.String(length=128), nullable=True),\n    sa.Column('x509_c', sa.String(length=2), nullable=True),\n    sa.PrimaryKeyConstraint('id')\n)\n\nDEMO_ORGANIZATION = {\n    'name': 'Commandment Inc',\n    'payload_prefix': 'dev.commandment',\n    'x509_c': 'US',\n    'x509_o': 'Commandment',\n    'x509_ou': 'MDM'\n}\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.create_table(*TABLE)\n\n\ndef schema_downgrades():\n    op.drop_table('organizations')\n\n\ndef data_upgrades():\n    tbl = table(*TABLE[:-1])\n\n    op.bulk_insert(tbl, [\n        DEMO_ORGANIZATION\n    ])\n\n\ndef data_downgrades():\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/e947cdf82307_add_ios_installed_application_fields.py",
    "content": "\"\"\"add ios installed application fields\n\nRevision ID: e947cdf82307\nRevises: 3061e56045eb\nCreate Date: 2018-07-01 20:30:53.621855\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'e947cdf82307'\ndown_revision = '3061e56045eb'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.add_column('installed_applications', sa.Column('adhoc_codesigned', sa.Boolean(), nullable=True))\n    op.add_column('installed_applications', sa.Column('appstore_vendable', sa.Boolean(), nullable=True))\n    op.add_column('installed_applications', sa.Column('beta_app', sa.Boolean(), nullable=True))\n    op.add_column('installed_applications', sa.Column('device_based_vpp', sa.Boolean(), nullable=True))\n    op.add_column('installed_applications', sa.Column('has_update_available', sa.Boolean(), nullable=True))\n    op.add_column('installed_applications', sa.Column('installing', sa.Boolean(), nullable=True))\n    op.create_index(op.f('ix_installed_applications_external_version_identifier'), 'installed_applications',\n                    ['external_version_identifier'], unique=False)\n\n\ndef schema_downgrades():\n    op.drop_index(op.f('ix_installed_applications_external_version_identifier'), table_name='installed_applications')\n    op.drop_column('installed_applications', 'installing')\n    op.drop_column('installed_applications', 'has_update_available')\n    op.drop_column('installed_applications', 'device_based_vpp')\n    op.drop_column('installed_applications', 'beta_app')\n    op.drop_column('installed_applications', 'appstore_vendable')\n    op.drop_column('installed_applications', 'adhoc_codesigned')\n\n\n# def data_upgrades():\n#     \"\"\"Add any optional data upgrade migrations here!\"\"\"\n#     pass\n#\n#\n# def data_downgrades():\n#     \"\"\"Add any optional data downgrade migrations here!\"\"\"\n#     pass\n"
  },
  {
    "path": "commandment/alembic/versions/e9b0a4f7b595_create_payloads_table.py",
    "content": "\"\"\"Create payloads table\n\nRevision ID: e9b0a4f7b595\nRevises: 0c4c448f4daf\nCreate Date: 2017-05-18 22:34:37.838655\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n# revision identifiers, used by Alembic.\nrevision = 'e9b0a4f7b595'\ndown_revision = '0c4c448f4daf'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('payloads',\n                    sa.Column('id', sa.Integer(), nullable=False),\n                    sa.Column('type', sa.String(), nullable=False),\n                    sa.Column('version', sa.Integer(), nullable=True),\n                    sa.Column('identifier', sa.String(), nullable=True),\n                    sa.Column('uuid', commandment.dbtypes.GUID(), nullable=False),\n                    sa.Column('display_name', sa.String(), nullable=True),\n                    sa.Column('description', sa.Text(), nullable=True),\n                    sa.Column('organization', sa.String(), nullable=True),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_index(op.f('ix_payloads_type'), 'payloads', ['type'], unique=False)\n    op.create_index(op.f('ix_payloads_uuid'), 'payloads', ['uuid'], unique=False)\n\n\ndef downgrade():\n    op.drop_index(op.f('ix_payloads_uuid'), table_name='payloads')\n    op.drop_index(op.f('ix_payloads_type'), table_name='payloads')\n    op.drop_table('payloads')\n"
  },
  {
    "path": "commandment/alembic/versions/ea34ae3f1e7e_create_profile_payloads_table.py",
    "content": "\"\"\"Create profile_payloads table\n\nRevision ID: ea34ae3f1e7e\nRevises: ba4849d8c8ad\nCreate Date: 2017-05-19 19:45:34.375475\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'ea34ae3f1e7e'\ndown_revision = 'ba4849d8c8ad'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('profile_payloads',\n                    sa.Column('profile_id', sa.Integer(), nullable=True),\n                    sa.Column('payload_id', sa.Integer(), nullable=True),\n                    sa.ForeignKeyConstraint(['payload_id'], ['payloads.id'], ),\n                    sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], )\n                    )\n\n\ndef downgrade():\n    op.drop_table('profile_payloads')\n"
  },
  {
    "path": "commandment/alembic/versions/f029ac1af3f0_create_vpp_accounts.py",
    "content": "\"\"\"Create vpp_accounts table\n\nRevision ID: f029ac1af3f0\nRevises: 8c866896f76e\nCreate Date: 2017-07-19 13:02:13.563903\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'f029ac1af3f0'\ndown_revision = '8c866896f76e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    op.create_table('vpp_accounts',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('stoken', sa.String(), nullable=False),\n    sa.Column('licenses_since_modified_token', sa.String(), nullable=True),\n    sa.Column('licenses_batch_token', sa.String(), nullable=True),\n    sa.Column('users_since_modified_token', sa.String(), nullable=True),\n    sa.Column('users_batch_token', sa.String(), nullable=True),\n    sa.PrimaryKeyConstraint('id')\n    )\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    op.drop_table('vpp_accounts')\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/f5237c7e2374_create_scep_config_table.py",
    "content": "\"\"\"Create scep_config table\n\nRevision ID: f5237c7e2374\nRevises: e58afdc17baa\nCreate Date: 2017-05-19 19:34:00.120370\n\n\"\"\"\nfrom alembic import op, context\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = 'f5237c7e2374'\ndown_revision = 'e58afdc17baa'\nbranch_labels = None\ndepends_on = None\n\nTABLE = ('scep_config',\n         sa.Column('id', sa.Integer(), nullable=False, autoincrement=True),\n         sa.Column('url', sa.String(), nullable=False),\n         sa.Column('challenge_enabled', sa.Boolean(), nullable=True),\n         sa.Column('challenge', sa.String(), nullable=True),\n         sa.Column('ca_fingerprint', sa.String(), nullable=True),\n         sa.Column('subject', sa.String(), nullable=False),\n         sa.Column('key_size', sa.Integer(), nullable=False),\n         sa.Column('key_type', sa.String(), nullable=False, server_default='RSA'),\n         sa.Column('key_usage', sa.Enum('Signing', 'Encryption', 'All', name='keyusage'), nullable=True),\n         sa.Column('retries', sa.Integer(), nullable=False),\n         sa.Column('retry_delay', sa.Integer(), nullable=False),\n         sa.Column('certificate_renewal_time_interval', sa.Integer(), nullable=False),\n         sa.PrimaryKeyConstraint('id')\n         )\n\nDEMO_SCEP_CONFIG = {\n    'url': 'http://localhost:5000',\n    'challenge_enabled': True,\n    'challenge': 'sekret',\n    'subject': 'CN=%HardwareUUID%',\n    'key_size': 2048,\n    'key_type': 'RSA',\n    'key_usage': 'All',\n    'retries': 3,\n    'retry_delay': 10,\n    'certificate_renewal_time_interval': 24\n}\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.create_table(*TABLE)\n\n\ndef schema_downgrades():\n    op.drop_table('scep_config')\n\n\ndef data_upgrades():\n    tbl = sa.table(*TABLE[:-1])\n\n    op.bulk_insert(tbl, [\n        DEMO_SCEP_CONFIG\n    ])\n\n\ndef data_downgrades():\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/f8eb70b3aa2b_create_application_manifests.py",
    "content": "\"\"\"create application manifests\n\nRevision ID: f8eb70b3aa2b\nRevises: d5b32b5cc74e\nCreate Date: 2018-03-13 21:21:31.277764\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'f8eb70b3aa2b'\ndown_revision = 'd5b32b5cc74e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_upgrades()\n\n\ndef downgrade():\n    # if context.get_x_argument(as_dictionary=True).get('data', None):\n    #     data_downgrades()\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    \"\"\"schema upgrade migrations go here.\"\"\"\n    # op.alter_column('application_manifest_checksums', 'application_manifest_id',\n    #            existing_type=sa.INTEGER(),\n    #            nullable=True)\n    #op.drop_constraint('uq_application_checksum_index', 'application_manifest_checksums', type_='unique')\n    #op.drop_constraint(None, 'application_manifest_checksums', type_='foreignkey')\n    #op.create_foreign_key(None, 'application_manifest_checksums', 'application_manifests', ['application_manifest_id'], ['id'])\n    # op.add_column('application_manifests', sa.Column('full_size_image_needs_shine', sa.Boolean(), nullable=True))\n    # op.add_column('application_manifests', sa.Column('full_size_image_url', sa.String(), nullable=True))\n    # op.alter_column('application_manifests', 'bundle_id',\n    #            existing_type=sa.VARCHAR(),\n    #            nullable=False)\n    op.create_index(op.f('ix_application_manifests_bundle_id'), 'application_manifests', ['bundle_id'], unique=False)\n    op.create_index(op.f('ix_application_manifests_bundle_version'), 'application_manifests', ['bundle_version'], unique=False)\n    # op.drop_constraint('uq_application_bundle_version', 'application_manifests', type_='unique')\n    #op.drop_column('application_manifests', 'full_image_url')\n    #op.drop_column('application_manifests', 'full_image_needs_shine')\n\n\ndef schema_downgrades():\n    \"\"\"schema downgrade migrations go here.\"\"\"\n    #op.add_column('application_manifests', sa.Column('full_image_needs_shine', sa.BOOLEAN(), nullable=True))\n    #op.add_column('application_manifests', sa.Column('full_image_url', sa.VARCHAR(), nullable=True))\n    # op.create_unique_constraint('uq_application_bundle_version', 'application_manifests', ['bundle_id', 'bundle_version'])\n    op.drop_index(op.f('ix_application_manifests_bundle_version'), table_name='application_manifests')\n    op.drop_index(op.f('ix_application_manifests_bundle_id'), table_name='application_manifests')\n    # op.alter_column('application_manifests', 'bundle_id',\n    #            existing_type=sa.VARCHAR(),\n    #            nullable=True)\n    # op.drop_column('application_manifests', 'full_size_image_url')\n    # op.drop_column('application_manifests', 'full_size_image_needs_shine')\n    #op.drop_constraint(None, 'application_manifest_checksums', type_='foreignkey')\n    #op.create_foreign_key(None, 'application_manifest_checksums', 'application_manifests', ['application_manifest_id'], ['id'], ondelete='CASCADE')\n    #op.create_unique_constraint('uq_application_checksum_index', 'application_manifest_checksums', ['application_manifest_id', 'checksum_index'])\n    # op.alter_column('application_manifest_checksums', 'application_manifest_id',\n    #            existing_type=sa.INTEGER(),\n    #            nullable=False)\n\n\ndef data_upgrades():\n    \"\"\"Add any optional data upgrade migrations here!\"\"\"\n    pass\n\n\ndef data_downgrades():\n    \"\"\"Add any optional data downgrade migrations here!\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/alembic/versions/fa4d91c6aacf_create_managed_applications_table.py",
    "content": "\"\"\"create_managed_applications_table\n\nRevision ID: fa4d91c6aacf\nRevises: 3dbf6db7f9eb\nCreate Date: 2019-01-10 10:01:10.750225\n\n\"\"\"\n\n# From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements\n\nfrom alembic import op\nimport sqlalchemy as sa\nimport commandment.dbtypes\n\n\nfrom alembic import context\n\n# revision identifiers, used by Alembic.\nrevision = 'fa4d91c6aacf'\ndown_revision = '3dbf6db7f9eb'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    schema_upgrades()\n\n\ndef downgrade():\n    schema_downgrades()\n\n\ndef schema_upgrades():\n    op.create_table('managed_applications',\n        sa.Column('id', sa.Integer(), nullable=False),\n        sa.Column('device_id', sa.Integer(), nullable=True),\n        sa.Column('bundle_id', sa.String(), nullable=True),\n        sa.Column('external_version_id', sa.Integer(), nullable=True),\n        sa.Column('has_configuration', sa.Boolean(), nullable=True),\n        sa.Column('has_feedback', sa.Boolean(), nullable=True),\n        sa.Column('is_validated', sa.Boolean(), nullable=True),\n        sa.Column('management_flags', sa.Integer(), nullable=True),\n        sa.Column('status', sa.String(), nullable=True),\n        sa.Column('application_id', sa.Integer(), nullable=True),\n        sa.Column('ia_command_id', sa.Integer(), nullable=True),\n        sa.ForeignKeyConstraint(['application_id'], ['applications.id'], ),\n        sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ),\n        sa.ForeignKeyConstraint(['ia_command_id'], ['commands.id'], ),\n        sa.PrimaryKeyConstraint('id')\n    )\n\n\ndef schema_downgrades():\n    op.drop_table('managed_applications')\n"
  },
  {
    "path": "commandment/api/__init__.py",
    "content": ""
  },
  {
    "path": "commandment/api/app_json.py",
    "content": "\"\"\"\nThis module contains API endpoints which do not fit with the JSON-API specification.\n\"\"\"\nimport io\nfrom flask import Blueprint, send_file, abort, current_app, jsonify, request, make_response\nfrom sqlalchemy.orm.exc import NoResultFound\nimport plistlib\nimport string\nfrom commandment.plistutil.nonewriter import dumps as dumps_none\nfrom base64 import urlsafe_b64encode\nfrom commandment.models import db, Organization, Device, Command\nfrom commandment.pki.models import Certificate, RSAPrivateKey\nfrom commandment.profiles.models import Profile\nfrom commandment.mdm import commands, Platform\nfrom .schema import OrganizationFlatSchema\nfrom commandment.profiles.schema import ProfileSchema\nfrom commandment.profiles.plist_schema import ProfileSchema as ProfilePlistSchema\n\nflat_api = Blueprint('flat_api', __name__)\n\n# @flat_api.errorhandler(400)\n# def bad_request(e):\n#     return jsonify({'errors': [\n#         {\n#             'status': '400',\n#             'title': str(e),\n#         }\n#     ]}\n\n\n@flat_api.route('/v1/organization', methods=['GET'])\ndef organization_get():\n    \"\"\"Retrieve information about the MDM home organization.\n\n    Only returns a pseudo JSON-API representation because the standard has no definition for\n    `singleton` resources.\n\n    \"\"\"\n    try:\n        o = db.session.query(Organization).one()\n    except NoResultFound:\n        return abort(400, 'No organization details found')\n    \n    org_schema = OrganizationFlatSchema()\n    result = org_schema.dumps(o)\n\n    return jsonify(result.data)\n\n\n@flat_api.route('/v1/download/certificates/<int:certificate_id>')\ndef download_certificate(certificate_id: int):\n    \"\"\"Download a certificate in PEM format\n\n    :reqheader Accept: application/x-pem-file\n    :reqheader Accept: application/x-x509-user-cert\n    :reqheader Accept: application/x-x509-ca-cert\n    :resheader Content-Type: application/x-pem-file\n    :resheader Content-Type: application/x-x509-user-cert\n    :resheader Content-Type: application/x-x509-ca-cert\n    :statuscode 200: OK\n    :statuscode 404: There is no certificate configured\n    :statuscode 400: Can't produce requested encoding\n    \"\"\"\n    c = db.session.query(Certificate).filter(Certificate.id == certificate_id).one()\n    bio = io.BytesIO(c.pem_data)\n\n    return send_file(bio, 'application/x-pem-file', True, 'certificate.pem')\n\n\n@flat_api.route('/v1/rsa_private_keys/<int:rsa_private_key_id>/download')\ndef download_key(rsa_private_key_id: int):\n    \"\"\"Download an RSA private key in PEM or DER format\n\n    :reqheader Accept: application/x-pem-file\n    :reqheader Accept: application/pkcs8\n    :resheader Content-Type: application/x-pem-file\n    :resheader Content-Type: application/pkcs8\n    :statuscode 200: OK\n    :statuscode 404: Not found\n    :statuscode 400: Can't produce requested encoding\n    \"\"\"\n    if not current_app.debug:\n        abort(500, 'Not supported in this mode')\n\n    c = db.session.query(RSAPrivateKey).filter(RSAPrivateKey.id == rsa_private_key_id).one()\n    bio = io.BytesIO(c.pem_data)\n\n    return send_file(bio, 'application/x-pem-file', True, 'rsa_private_key.pem')\n\n\n@flat_api.route('/v1/devices/test/<int:device_id>', methods=['POST'])\ndef device_test(device_id: int):\n    \"\"\"Testing endpoint for quick and dirty command checking\"\"\"\n    d = db.session.query(Device).filter(Device.id == device_id).one()\n\n    #ia = commands.InstallApplication(ManifestURL='https://localhost:5443/static/appmanifest/munkitools-3.1.0.3430.plist')\n    ia = commands.Settings(bluetooth=False)\n\n    dbc = Command.from_model(ia)\n    dbc.device = d\n    db.session.add(dbc)\n\n    db.session.commit()\n\n    return 'OK'\n\n\n@flat_api.route('/v1/devices/inventory/<int:device_id>')\ndef device_inventory(device_id: int):\n    \"\"\"Enqueue ALL inventory commands to refresh the device's entire inventory.\n    \n    :statuscode 200: OK\n    \"\"\"\n    d = db.session.query(Device).filter(Device.id == device_id).one()\n\n    # DeviceInformation\n    di = commands.DeviceInformation.for_platform(d.platform, d.os_version)\n    db_command = Command.from_model(di)\n    db_command.device = d\n    db.session.add(db_command)\n\n    # InstalledApplicationList - Pretty taxing so don't run often\n    # ial = commands.InstalledApplicationList()\n    # db_command_ial = Command.from_model(ial)\n    # db_command_ial.device = d\n    # db.session.add(db_command_ial)\n\n    # CertificateList\n    cl = commands.CertificateList()\n    dbc = Command.from_model(cl)\n    dbc.device = d\n    db.session.add(dbc)\n\n    # SecurityInfo\n    si = commands.SecurityInfo()\n    dbsi = Command.from_model(si)\n    dbsi.device = d\n    db.session.add(dbsi)\n\n    # ProfileList\n    pl = commands.ProfileList()\n    db_pl = Command.from_model(pl)\n    db_pl.device = d\n    db.session.add(db_pl)\n\n    # AvailableOSUpdates\n    au = commands.AvailableOSUpdates()\n    au_pl = Command.from_model(au)\n    au_pl.device = d\n    db.session.add(au_pl)\n\n    mal = commands.ManagedApplicationList()\n    mal_pl = Command.from_model(mal)\n    mal_pl.device = d\n    db.session.add(mal_pl)\n\n    db.session.commit()\n\n    return 'OK'\n\n\n@flat_api.route('/v1/devices/<int:device_id>/clear_passcode', methods=['POST'])\ndef clear_passcode(device_id: int):\n    \"\"\"Enqueues a ClearPasscode command for the device id specified.\n\n    :reqheader Accept: application/vnd.api+json\n    :reqheader Content-Type: ?\n    :resheader Content-Type: application/vnd.api+json\n    :statuscode 201: command created\n    :statuscode 400: not applicable to this device\n    :statuscode 404: device with this identifier was not found\n    :statuscode 500: system error\n    \"\"\"\n    d = db.session.query(Device).filter(Device.id == device_id).one()\n    if d.platform == Platform.macOS:\n        return abort(400, 'ClearPasscode is not supported on Mac computers')\n\n    if d.unlock_token is None:\n        return abort(400, 'No UnlockToken is available for this device')\n\n    cp = commands.ClearPasscode(UnlockToken=urlsafe_b64encode(d.unlock_token).decode('utf-8'))\n    cp_pl = Command.from_model(cp)\n    cp_pl.device = d\n    db.session.add(cp_pl)\n\n    db.session.commit()\n\n    return 'OK', 201, {}\n\n\n@flat_api.route('/v1/devices/<int:device_id>/lock', methods=['POST'])\ndef lock(device_id: int):\n    \"\"\"Enqueues a DeviceLock command for the device id specified.\n\n    If the target device is a macOS device, a 6 digit Find My Mac PIN will be automatically generated and stored\n    with the device record (and also output in the response).\n\n    :reqheader Accept: application/vnd.api+json\n    :reqheader Content-Type: ?\n    :resheader Content-Type: application/vnd.api+json\n    :statuscode 201: command created\n    :statuscode 400: not applicable to this device\n    :statuscode 404: device with this identifier was not found\n    :statuscode 500: system error\n    \"\"\"\n    d = db.session.query(Device).filter(Device.id == device_id).one()\n    if d.platform == Platform.macOS:\n        return abort(400, 'Not Implemented')\n\n    dl = commands.DeviceLock()\n    dl_pl = Command.from_model(dl)\n    dl_pl.device = d\n    db.session.add(dl_pl)\n\n    db.session.commit()\n\n    return 'OK', 201, {}\n\n\n@flat_api.route('/v1/devices/<int:device_id>/restart', methods=['POST'])\ndef restart(device_id: int):\n    \"\"\"Enqueues a RestartDevice command for the device id specified.\n\n    :reqheader Accept: application/json\n    :reqheader Content-Type: application/json\n    :resheader Content-Type: application/json\n    :statuscode 201: command created\n    :statuscode 400: not applicable to this device. returned if this device is not supervised or not capable of taking command.\n    :statuscode 404: device with this identifier was not found\n    :statuscode 500: system error\n    \"\"\"\n    d: Device = db.session.query(Device).filter(Device.id == device_id).one()\n\n    if d.model_name == 'iPhone' and not d.is_supervised:\n        return 'Cannot restart an unsupervised iOS device', 400, {}\n\n    cmd = commands.RestartDevice()\n    orm_cmd = Command.from_model(cmd)\n    orm_cmd.device = d\n    db.session.add(orm_cmd)\n\n    db.session.commit()\n\n    return 'OK'\n\n\n@flat_api.route('/v1/devices/<int:device_id>/shutdown', methods=['POST'])\ndef shutdown(device_id: int):\n    \"\"\"Enqueues a Shutdown command for the device id specified.\n\n    :reqheader Accept: application/json\n    :reqheader Content-Type: application/json\n    :resheader Content-Type: application/json\n    :statuscode 201: command created\n    :statuscode 400: not applicable to this device\n    :statuscode 404: device with this identifier was not found\n    :statuscode 500: system error\n    \"\"\"\n    d = db.session.query(Device).filter(Device.id == device_id).one()\n\n    if d.model_name == 'iPhone' and not d.is_supervised:\n        return 'Cannot shut down an unsupervised iOS device', 400, {}\n\n    cmd = commands.ShutDownDevice()\n    orm_cmd = Command.from_model(cmd)\n    orm_cmd.device = d\n    db.session.add(orm_cmd)\n\n    db.session.commit()\n\n    return 'OK'\n\n\n@flat_api.route('/v1/upload/profiles', methods=['POST'])\ndef upload_profile():\n    \"\"\"Upload a custom profile using multipart/form-data I.E from an upload input.\n\n    Encrypted profiles are not supported.\n\n    The profiles contents will be stored using the following process:\n    - For the top level profile (and each payload) there is a marshmallow schema which maps the payload keys into\n        the SQLAlchemy model keys. It is also the responsibility of the marshmallow schema to be the validator for \n        uploaded profiles.\n    - The profile itself is inserted as a Profile model.\n    - Each payload is unmarshalled using marshmallow to a specific Payload model. Each specific model contains a join\n        table inheritance to the base ``payloads`` table.\n\n    The returned body contains a jsonapi object with details of the newly created profile and associated payload ID's.\n\n    Note: Does not support ``application/x-www-form-urlencoded``\n\n    TODO:\n        - Support signed profiles\n\n    :reqheader Accept: application/vnd.api+json\n    :reqheader Content-Type: multipart/form-data\n    :resheader Content-Type: application/vnd.api+json\n    :statuscode 201: profile created\n    :statuscode 400: If the request contained malformed or missing payload data.\n    :statuscode 500: If something else went wrong with parsing or persisting the payload(s)\n    \"\"\"\n    if 'file' not in request.files:\n        abort(400, 'no file uploaded in request data')\n\n    f = request.files['file']\n\n    if not f.content_type == 'application/x-apple-aspen-config':\n        abort(400, 'incorrect MIME type in request')\n\n    try:\n        data = f.read()\n        plist = plistlib.loads(data)\n\n        profile = ProfilePlistSchema().load(plist).data\n    except plistlib.InvalidFileException as e:\n        current_app.logger.error(e)\n        abort(400, 'invalid plist format supplied')\n\n    except BaseException as e:  # TODO: separate errors for exceptions caught here\n        current_app.logger.error(e)\n        abort(400, 'cannot parse the supplied profile')\n\n    profile.data = data\n    db.session.add(profile)\n    db.session.commit()\n\n    profile_schema = ProfileSchema()\n    model_data = profile_schema.dump(profile).data\n    resp = make_response(jsonify(model_data), 201, {'Content-Type': 'application/vnd.api+json'})\n    return resp\n\n\n@flat_api.route('/v1/download/profiles/<int:profile_id>')\ndef download_profile(profile_id: int):\n    \"\"\"Download a profile.\n    \n    The profile is reconstructed from its database representation.\n    \n    Args:\n        profile_id (int): The profile id \n\n    :reqheader Accept: application/x-apple-aspen-config\n    :resheader Content-Type: application/x-apple-aspen-config\n    :statuscode 200:\n    :statuscode 404:\n    :statuscode 500:\n    \"\"\"\n    try:\n        profile = db.session.query(Profile).filter(Profile.id == profile_id).one()\n    except NoResultFound:\n        abort(404)\n\n    return profile.data, 200, {'Content-Type': 'application/x-apple-aspen-config'}\n\n"
  },
  {
    "path": "commandment/api/app_jsonapi.py",
    "content": "\"\"\"\n    This module contains all of the API generated using the Flask-REST-JSONAPI extension.\n\"\"\"\nfrom flask import Blueprint\nfrom flask_rest_jsonapi import Api\nfrom .resources import CertificatesList, CertificateDetail, CertificateSigningRequestList, \\\n    CertificateSigningRequestDetail, PushCertificateList, SSLCertificatesList, \\\n    CACertificateList, PrivateKeyDetail, DeviceList, DeviceDetail, \\\n    DeviceRelationship, \\\n    TagsList, TagDetail, TagRelationship\n\n\n# PayloadsList, PayloadDetail,\n\napi_app = Blueprint('api_app', __name__)\napi = Api(blueprint=api_app)\n\n# Certificates\napi.route(CertificatesList, 'certificates_list', '/v1/certificates/')\napi.route(CertificateDetail, 'certificate_detail', '/v1/certificates/<int:certificate_id>')\n\napi.route(CertificateSigningRequestList, 'certificate_signing_request_list', '/v1/certificate_signing_requests')\napi.route(CertificateSigningRequestDetail, 'certificate_signing_request_detail',\n          '/v1/certificate_signing_requests/<int:certificate_signing_request_id>')\napi.route(PushCertificateList, 'push_certificates_list', '/v1/push_certificates/')\napi.route(SSLCertificatesList, 'ssl_certificates_list', '/v1/ssl_certificates/')\napi.route(CACertificateList, 'ca_certificates_list', '/v1/ca_certificates/')\napi.route(PrivateKeyDetail, 'private_key_detail', '/v1/rsa_private_keys/<int:private_key_id>')\n\n\n# Devices\napi.route(DeviceList, 'devices_list', '/v1/devices', '/v1/device_groups/<int:device_group_id>/devices',\n          '/v1/dep/profiles/<int:dep_profile_id>/devices',\n          '/v1/managed_applications/<int:managed_application_id>/devices')\napi.route(DeviceDetail, 'device_detail', '/v1/devices/<int:device_id>')\napi.route(DeviceRelationship, 'device_commands', '/v1/devices/<int:device_id>/relationships/commands')\napi.route(DeviceRelationship, 'device_tags', '/v1/devices/<int:device_id>/relationships/tags')\napi.route(DeviceRelationship, 'device_dep_profile', '/v1/devices/<int:device_id>/relationships/dep_profile')\n\n# Organizations\n# api.route(OrganizationList, 'organizations_list', '/v1/organizations')\n# api.route(OrganizationDetail, 'organization_detail', '/v1/organizations/<int:organization_id>')\n\n# Tags\napi.route(TagsList, 'tags_list', '/v1/tags', '/v1/devices/<int:device_id>/tags')\napi.route(TagDetail, 'tag_detail', '/v1/tags/<int:tag_id>')\napi.route(TagRelationship, 'tag_devices', '/v1/tags/<int:tag_id>/relationships/devices')\n\n"
  },
  {
    "path": "commandment/api/configuration.py",
    "content": "\"\"\"\nThis module contains a Blueprint for API endpoints relating to system configuration.\n\"\"\"\nfrom flask import Blueprint, abort, jsonify, request\nfrom sqlalchemy.orm.exc import NoResultFound\nfrom commandment.models import db, Organization, SCEPConfig\nfrom .schema import OrganizationFlatSchema, SCEPConfigFlatSchema\nfrom commandment.profiles.schema import ProfileSchema\n\nconfiguration_app = Blueprint('configuration_app', __name__)\n\n\n@configuration_app.route('/organization', methods=['GET'])\ndef organization_get():\n    \"\"\"Retrieve information about the MDM home organization.\n\n    :reqheader Accept: application/json\n    :reqheader Content-Type: application/json\n    :resheader Content-Type: application/json\n    :statuscode 200: Success\n    :statuscode 404: No configuration available\n    :statuscode 500: Other error\n    \"\"\"\n    try:\n        o = db.session.query(Organization).one()\n    except NoResultFound:\n        return abort(404, 'No organization details found')\n\n    schema = OrganizationFlatSchema()\n    dump = schema.dumps(o)\n\n    return dump.data, 200, {'Content-Type': 'application/json'}\n\n\n@configuration_app.route('/organization', methods=['PATCH', 'POST'])\ndef organization_post():\n    \"\"\"Update information about the MDM home organization.\n\n    :reqheader Accept: application/json\n    :reqheader Content-Type: application/json\n    :resheader Content-Type: application/json\n    :statuscode 201: Success\n    :statuscode 400: Validation Error\n    :statuscode 500: Other error\n    \"\"\"\n    schema = OrganizationFlatSchema()\n    data = request.data\n    result = schema.loads(data)\n    db.session.commit()\n\n    dump = schema.dumps(result.data)\n\n    return dump.data, 200, {'Content-Type': 'application/json'}\n\n\n@configuration_app.route('/scep', methods=['GET'])\ndef scep_get():\n    \"\"\"Retrieve information about SCEP enrollment configuration\n\n    :reqheader Accept: application/json\n    :reqheader Content-Type: application/json\n    :resheader Content-Type: application/json\n    :statuscode 200: Success\n    :statuscode 404: No configuration available\n    :statuscode 500: Other error\n    \"\"\"\n    try:\n        c = db.session.query(SCEPConfig).one()\n    except NoResultFound:\n        return abort(404, 'No organization details found')\n\n    schema = SCEPConfigFlatSchema()\n    dump = schema.dumps(c)\n\n    return dump.data, 200, {'Content-Type': 'application/json'}\n\n\n@configuration_app.route('/scep', methods=['PATCH', 'POST'])\ndef scep_post():\n    \"\"\"Update information about SCEP enrollment configuration\n\n    :reqheader Accept: application/json\n    :reqheader Content-Type: application/json\n    :resheader Content-Type: application/json\n    :statuscode 201: Success\n    :statuscode 400: Validation Error\n    :statuscode 500: Other error\n    \"\"\"\n    schema = SCEPConfigFlatSchema()\n    data = request.data\n    result = schema.loads(data)\n    db.session.commit()\n\n    dump = schema.dumps(result.data)\n\n    return dump.data, 200, {'Content-Type': 'application/json'}\n"
  },
  {
    "path": "commandment/api/resources.py",
    "content": "\"\"\"\n    This module defines resources, as required by the Flask-REST-JSONAPI package. This represents most of the REST API.\n\"\"\"\nfrom flask_rest_jsonapi.exceptions import ObjectNotFound\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom .schema import DeviceSchema, CertificateSchema, PrivateKeySchema, \\\n    CertificateSigningRequestSchema, OrganizationSchema, TagSchema\nfrom commandment.models import db, Device, Organization, Tag, Command\nfrom commandment.pki.models import Certificate, CertificateSigningRequest, SSLCertificate, PushCertificate, \\\n    CACertificate\n\nfrom commandment.mdm import commands as mdmcommands, CommandType\nfrom commandment.auth import oauth2\n\nfrom flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship\n\n\nclass DeviceList(ResourceList):\n    # decorators = (oauth2.require_oauth(''),)\n    schema = DeviceSchema\n    data_layer = {'session': db.session, 'model': Device}\n\n\nclass DeviceDetail(ResourceDetail):\n    schema = DeviceSchema\n    data_layer = {\n        'session': db.session,\n        'model': Device,\n        'url_field': 'device_id'\n    }\n\n    def before_patch(self, args, kwargs, data=None):\n        \"\"\"Custom logic when updating a device:\n\n        - If the `device_name` field would change, we queue a new Settings command to change the name of the device.\n        - If there already was an undelivered Settings command, it should be replaced by the new one.\n        - If the `hostname` field would change, that should also be sent via a Settings command (will be coalesced with Device Name).\n        \"\"\"\n        if 'device_name' in data or 'hostname' in data:\n            # settings_commands = self.data_layer['model'].commands\n            cmd: mdmcommands.Settings = mdmcommands.Command.new_request_type(\"Settings\", {})\n\n            if 'device_name' in data:\n                cmd.device_name = data['device_name']\n                del data['device_name']\n\n            if 'hostname' in data:\n                cmd.hostname = data['hostname']\n                del data['hostname']\n\n            model = Command.from_model(cmd)\n            self.data_layer['session'].add(model)\n\n\nclass DeviceRelationship(ResourceRelationship):\n    schema = DeviceSchema\n    data_layer = {\n        'session': db.session,\n        'model': Device,\n        'url_field': 'device_id'\n    }\n\n    def before_post(self, args, kwargs, json_data=None):\n        \"\"\"Custom logic for relationship management:\n\n        - If the dep_profile relationship is created, we need to check if the DEP Profile exists or has been uploaded yet.\n        \"\"\"\n        pass\n\n    def before_patch(self, args, kwargs, json_data=None):\n        pass\n\n    def after_patch(self, result):\n        \"\"\"Device relationship post-processing:\n\n        - If `dep_profiles` relationship was changed, update the DEP profile on the apple side.\n        \"\"\"\n        pass\n        # dep = get_dep()\n\n\n\nclass CertificatesList(ResourceList):\n    schema = CertificateSchema\n    data_layer = {'session': db.session, 'model': Certificate}\n\n\nclass CertificateDetail(ResourceDetail):\n    schema = CertificateSchema\n    data_layer = {'session': db.session, 'model': Certificate}\n\n\nclass CertificateTypeDetail(ResourceDetail):\n    schema = CertificateSchema\n    data_layer = {'session': db.session, 'model': Certificate}\n\n\nclass PrivateKeyDetail(ResourceDetail):\n    schema = PrivateKeySchema\n    data_layer = {'session': db.session, 'model': Certificate}\n\n\nclass CertificateSigningRequestList(ResourceList):\n    schema = CertificateSigningRequestSchema\n    data_layer = {\n        'session': db.session,\n        'model': CertificateSigningRequest,\n    }\n\n\nclass CertificateSigningRequestDetail(ResourceDetail):\n    schema = CertificateSigningRequestSchema\n    data_layer = {\n        'session': db.session,\n        'model': CertificateSigningRequest\n    }\n\n\nclass PushCertificateList(ResourceList):\n    schema = CertificateSchema\n    data_layer = {\n        'session': db.session,\n        'model': PushCertificate\n    }\n\n\nclass CACertificateList(ResourceList):\n    schema = CertificateSchema\n    data_layer = {\n        'session': db.session,\n        'model': CACertificate\n    }\n\n\nclass SSLCertificatesList(ResourceList):\n    schema = CertificateSchema\n    data_layer = {\n        'session': db.session,\n        'model': SSLCertificate\n    }\n\n\nclass OrganizationList(ResourceList):\n    schema = OrganizationSchema\n    data_layer = {\n        'session': db.session,\n        'model': Organization\n    }\n\n\nclass OrganizationDetail(ResourceDetail):\n    schema = OrganizationSchema\n    data_layer = {\n        'session': db.session,\n        'model': Organization\n    }\n\n\nclass TagsList(ResourceList):\n    schema = TagSchema\n    data_layer = {\n        'session': db.session,\n        'model': Tag\n    }\n    view_kwargs = True\n\n\nclass TagDetail(ResourceDetail):\n    schema = TagSchema\n    data_layer = {\n        'session': db.session,\n        'model': Tag,\n        'url_field': 'tag_id'\n    }\n\n\nclass TagRelationship(ResourceRelationship):\n    schema = TagSchema\n    data_layer = {\n        'session': db.session,\n        'model': Tag,\n        'url_field': 'tag_id'\n    }\n\n\n\n\n"
  },
  {
    "path": "commandment/api/schema.py",
    "content": "\"\"\"\n    This module contains schema definitions for Marshmallow-JSONAPI and therefore Flask-REST-JSONAPI.\n    It also contains non subpackage specific JSON schema definitions.\n\"\"\"\n\nfrom marshmallow_jsonapi import fields\nfrom marshmallow_jsonapi.flask import Relationship, Schema\nfrom marshmallow import Schema as FlatSchema, post_load\nfrom commandment.models import db, Organization, SCEPConfig\n\n\nclass DeviceSchema(Schema):\n    class Meta:\n        type_ = 'devices'\n        self_view = 'api_app.device_detail'\n        self_view_kwargs = {'device_id': '<id>'}\n        self_view_many = 'api_app.devices_list'\n        strict = True\n\n    id = fields.Int(dump_only=True)\n    udid = fields.Str(dump_only=True)\n    topic = fields.Str()\n\n    build_version = fields.Str()\n\n    # device_name and hostname are \"pseudo\" read-only in that writing them does not affect the field but enqueues\n    # an MDM command to change the name.\n    device_name = fields.Str()\n    hostname = fields.Str()\n    local_hostname = fields.Str(dump_only=True)\n\n    model = fields.Str()\n    model_name = fields.Str()\n    os_version = fields.Str()\n    product_name = fields.Str()\n    serial_number = fields.Str()\n\n    awaiting_configuration = fields.Bool()\n    last_seen = fields.DateTime(dump_only=True)\n\n    available_device_capacity = fields.Float()\n    device_capacity = fields.Float()\n    wifi_mac = fields.Str()\n    bluetooth_mac = fields.Str()\n\n    # private\n    # push_magic = fields.Str()\n    # token = fields.Str()\n    # unlock_token = fields.Str()\n    tokenupdate_at = fields.DateTime()\n\n    # SecurityInfo\n    passcode_present = fields.Bool()\n    passcode_compliant = fields.Bool()\n    passcode_compliant_with_profiles = fields.Bool()\n    fde_enabled = fields.Bool()\n    fde_has_prk = fields.Bool()\n    fde_has_irk = fields.Bool()\n    firewall_enabled = fields.Bool()\n    block_all_incoming = fields.Bool()\n    stealth_mode_enabled = fields.Bool()\n    sip_enabled = fields.Bool()\n\n    battery_level = fields.Float(dump_only=True)\n    imei = fields.Str(dump_only=True)\n\n    is_cloud_backup_enabled = fields.Bool(dump_only=True)\n    itunes_store_account_is_active = fields.Bool(dump_only=True)\n\n    last_cloud_backup_date = fields.DateTime(dump_only=True)\n\n    # TODO: Relationship to dep_config\n\n    # certificate = Relationship(\n    #     self_view='api_app.device_certificate',\n    #     self_view_kwargs={'certificate_id': '<id>'},\n    #     related_view='api_app.certificate_detail',\n    #     related_view_kwargs={'certificate_id': '<id>'},\n    # )\n\n    # DEP\n    is_dep = fields.Bool()\n    description = fields.Str(dump_only=True)\n    color = fields.Str(dump_only=True)\n    asset_tag = fields.Str(dump_only=True)\n    profile_status = fields.Str(dump_only=True)\n    profile_uuid = fields.UUID(dump_only=True)\n    profile_assign_time = fields.DateTime(dump_only=True)\n    profile_push_time = fields.DateTime(dump_only=True)\n    device_assigned_date = fields.DateTime(dump_only=True)\n    device_assigned_by = fields.Str(dump_only=True)\n    os = fields.Str(dump_only=True)\n    device_family = fields.Str(dump_only=True)\n\n    commands = Relationship(\n        related_view='api_app.commands_list',\n        related_view_kwargs={'device_id': '<id>'},\n        many=True,\n        schema='CommandSchema',\n        type_='commands'\n    )\n\n    installed_certificates = Relationship(\n        related_view='api_app.installed_certificates_list',\n        related_view_kwargs={'device_id': '<id>'},\n        many=True,\n        schema='InstalledCertificateSchema',\n        type_='installed_certificates'\n    )\n\n    installed_applications = Relationship(\n        related_view='api_app.installed_applications_list',\n        related_view_kwargs={'device_id': '<id>'},\n        many=True,\n        schema='InstalledApplicationSchema',\n        type_='installed_applications'\n    )\n\n    tags = Relationship(\n        related_view='api_app.tags_list',\n        related_view_kwargs={'device_id': '<id>'},\n        many=True,\n        schema='TagSchema',\n        type_='tags'\n    )\n\n    available_os_updates = Relationship(\n        related_view='api_app.available_os_updates_list',\n        related_view_kwargs={'device_id': '<id>'},\n        many=True,\n        schema='AvailableOSUpdateSchema',\n        type_='available_os_updates'\n    )\n\n    dep_profile = Relationship(\n        related_view='dep_app.dep_profile_detail',\n        related_view_kwargs={'dep_profile_id': '<dep_profile_id>'},\n        many=False,\n        schema='DEPProfileSchema',\n        type_='dep_profiles',\n    )\n\n\nclass PrivateKeySchema(Schema):\n    class Meta:\n        type_ = 'private_keys'\n        self_view = 'api_app.private_key_detail'\n        self_view_kwargs = {'private_key_id': '<id>'}\n        strict = True\n\n    id = fields.Int(dump_only=True)\n    pem_key = fields.Str()\n\n\nclass CertificateSchema(Schema):\n    class Meta:\n        type_ = 'certificates'\n        self_view = 'api_app.certificate_detail'\n        self_view_kwargs = {'certificate_id': '<id>'}\n        self_view_many = 'api_app.certificates_list'\n        strict = True\n\n    id = fields.Int(dump_only=True)\n    type = fields.Str(attribute='type')\n    x509_cn = fields.Str(dump_only=True)\n    not_before = fields.DateTime(dump_only=True)\n    not_after = fields.DateTime(dump_only=True)\n    # fingerprint = fields.Str(dump_only=True)\n    pem_certificate = fields.Str()\n\n    private_key = Relationship(\n        self_view='api_app.certificate_private_keys',\n        self_view_kwargs={'id': '<id>'},\n        related_view='api_app.private_key_detail',\n        related_view_kwargs={'private_key_id': '<id>'},\n        many=False,\n        schema='PrivateKeySchema',\n        type_='private_keys'\n    )\n\n\nclass CertificateSigningRequestSchema(Schema):\n    class Meta:\n        type_ = 'certificate_signing_requests'\n        self_view = 'api_app.certificate_signing_request_detail'\n        self_view_kwargs = {'certificate_signing_request_id': '<id>'}\n        self_view_many = 'api_app.certificate_signing_request_list'\n\n    id = fields.Int(dump_only=True)\n    purpose = fields.Str(load_only=True, attribute='req_type')\n    subject = fields.Str()\n    pem_request = fields.Str()\n\n\nclass OrganizationSchema(Schema):\n    class Meta:\n        type_ = 'organizations'\n        self_view = 'api_app.organization_detail'\n        self_view_kwargs = {'organization_id': '<id>'}\n\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n    payload_prefix = fields.Str()\n\n    x509_ou = fields.Str()\n    x509_o = fields.Str()\n    x509_st = fields.Str()\n    x509_c = fields.Str()\n\n\nclass OrganizationFlatSchema(FlatSchema):\n    name = fields.Str(required=True)\n    payload_prefix = fields.Str(required=True)\n\n    x509_ou = fields.Str()\n    x509_o = fields.Str()\n    x509_st = fields.Str()\n    x509_c = fields.Str()\n\n    @post_load\n    def make_organization(self, data: dict) -> Organization:\n        \"\"\"Construct a model from a parsed JSON schema.\"\"\"\n        rows = db.session.query(Organization).count()\n        \n        if rows == 1:\n            db.session.query(Organization).update(data)\n            o = db.session.query(Organization).first()\n        else:\n            o = Organization(**data)\n            db.session.add(o)\n\n        return o\n\n    \nclass SCEPConfigFlatSchema(FlatSchema):\n    source_type = fields.String()\n    url = fields.Url(relative=False, schemes=['http', 'https'], required=True)\n    challenge_enabled = fields.Boolean()\n    ca_fingerprint = fields.String()\n    subject = fields.String()\n    key_size = fields.Integer()\n    key_type = fields.String(dump_only=True)\n    key_usage = fields.Integer()\n    subject_alt_name = fields.String()\n    retries = fields.Integer()\n    retry_delay = fields.Integer()\n    certificate_renewal_time_interval = fields.Integer()\n\n    @post_load\n    def make_scepconfig(self, data: dict) -> SCEPConfig:\n        \"\"\"Construct a model from a parsed JSON schema.\"\"\"\n        rows = db.session.query(SCEPConfig).count()\n\n        if rows == 1:\n            db.session.query(SCEPConfig).update(data)\n            o = db.session.query(SCEPConfig).first()\n        else:\n            o = SCEPConfig(**data)\n            db.session.add(o)\n\n        return o\n\n\n\n\nclass TagSchema(Schema):\n    class Meta:\n        type_ = 'tags'\n        self_view = 'api_app.tag_detail'\n        self_view_kwargs = {'tag_id': '<id>'}\n        self_view_many = 'api_app.tags_list'\n\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n    color = fields.Str()\n\n    devices = Relationship(\n        self_view='api_app.tag_devices',\n        self_view_kwargs={'tag_id': '<id>'},\n        related_view='api_app.device_detail',\n        related_view_kwargs={'device_id': '<id>'},\n        schema='DeviceSchema',\n        many=True,\n        type_='devices'\n    )\n\n    # profiles = Relationship(\n    #     related_view='api_app.profiles_list',\n    #     related_view_kwargs={'profile_id': '<id>'},\n    #     schema='ProfileSchema',\n    #     many=True,\n    #     type_='profiles'\n    # )\n"
  },
  {
    "path": "commandment/apns/__init__.py",
    "content": ""
  },
  {
    "path": "commandment/apns/app.py",
    "content": "from datetime import datetime\nfrom flask import Blueprint, request, abort, current_app, jsonify\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom commandment.errors import JSONAPIError\nfrom commandment.models import db, Device\nfrom commandment.pki.models import RSAPrivateKey, CertificateSigningRequest, CACertificate, \\\n    EncryptionCertificate\nfrom commandment.pki import ssl as cmdssl\nfrom .push import push_to_device\nfrom .schema import PushResponseFlatSchema\nfrom .mdmcert import submit_mdmcert_request, decrypt_mdmcert\nimport ssl\n\napi_push_app = Blueprint('api_push_app', __name__)\n\nMDMCERT_REQ_URL = 'https://mdmcert.download/api/v1/signrequest'\n\n# PLEASE! Do not take this key and use it for another product/project. It's\n# only for Commandment's use. If you'd like to get your own (free!) key\n# contact the mdmcert.download administrators and get your own key for your\n# own project/product.  We're trying to keep statistics on which products are\n# requesting certs (per Apple T&C). Don't force Apple's hand and\n# ruin it for everyone!\nMDMCERT_API_KEY = 'b742461ff981756ca3f924f02db5a12e1f6639a9109db047ead1814aafc058dd'\n\n\n@api_push_app.route('/v1/devices/<int:device_id>/push', methods=['POST', 'GET'])\ndef push(device_id: int):\n    \"\"\"Send a (Blank) push notification to the specified device by its Commandment ID.\n    \n    This causes the device to check back with the MDM for pending commands.\n\n    :statuscode 400: impossible to push to device (no token or invalid token)\n    :statuscode 404: device does not exist\n    :statuscode 200: push complete\n    \"\"\"\n    device = db.session.query(Device).filter(Device.id == device_id).one()\n    if device.token is None or device.push_magic is None:\n        abort(jsonify(error=True, message='Cannot request push on a device that has no device token or push magic'))\n\n    try:\n        response = push_to_device(device)\n    except ssl.SSLError:\n        return abort(400, jsonify(error=True, message=\"The push certificate has expired\"))\n\n    current_app.logger.info(\"[APNS2 Response] Status: %d, Reason: %s, APNS ID: %s, Timestamp\",\n                            response.status_code, response.reason, response.apns_id.decode('utf-8'))\n    device.last_push_at = datetime.utcnow()\n    if response.status_code == 200:\n        device.last_apns_id = response.apns_id\n        \n    db.session.commit()\n    push_res_schema = PushResponseFlatSchema()\n    result = push_res_schema.dumps(response)\n\n    return result\n\n\n@api_push_app.route('/v1/mdmcert/request/<string:email>', methods=['GET'])\ndef mdmcert_request(email: str):\n    \"\"\"Ask the mdmcert.download service to generate a new Certificate Signing Request for the given e-mail address.\n\n    If an encryption certificate does not exist on the system, one will be generated to process the resulting encrypted\n    and signed CSR. The common name of the certificate will be the e-mail address that is registered with the\n    mdmcert.download service, and the type will be an EncryptionCertificate.\n\n    :reqheader Accept: application/json\n    :resheader Content-Type: application/json\n    \"\"\"\n    try:\n        apns_csr_model = db.session.query(CertificateSigningRequest).\\\n            filter(CertificateSigningRequest.x509_cn == \"commandment-apns\").one()\n    except NoResultFound:\n        private_key, csr = cmdssl.generate_signing_request('commandment-apns')\n        private_key_model = RSAPrivateKey.from_crypto(private_key)\n        db.session.add(private_key_model)\n        apns_csr_model = CertificateSigningRequest.from_crypto(csr)\n        apns_csr_model.rsa_private_key = private_key_model\n        db.session.add(apns_csr_model)\n        db.session.commit()\n\n    try:\n        encrypt_cert_model = db.session.query(EncryptionCertificate).\\\n            filter(EncryptionCertificate.x509_cn == email).one()\n    except NoResultFound:\n        encrypt_key, encrypt_with_cert = cmdssl.generate_self_signed_certificate(email)\n        encrypt_key_model = RSAPrivateKey.from_crypto(encrypt_key)\n        db.session.add(encrypt_key_model)\n        encrypt_cert_model = EncryptionCertificate.from_crypto(encrypt_with_cert)\n        encrypt_cert_model.rsa_private_key = encrypt_key_model\n        db.session.add(encrypt_cert_model)\n        db.session.commit()\n\n    current_app.logger.info(\"Submitting request to mdmcert.download for %s\", email)\n    mdmcert_result = submit_mdmcert_request(\n        email=email,\n        csr_pem=apns_csr_model.pem_data,\n        encrypt_with_pem=encrypt_cert_model.pem_data,\n    )\n\n    return jsonify(mdmcert_result)\n\n\n@api_push_app.route('/v1/mdmcert/decrypt', methods=['POST'])\ndef mdmcert_decrypt():\n    \"\"\"Upload the encrypted, signed request from mdmcert.download that was received via e-mail.\n\n    The filename looks something like :file:`mdm_signed_request.YYMMDD_HHMMSS_NNN.plist.b64.p7`\n    It is a hex-encoded PKCS#7 message.\n\n    :reqheader Accept: application/json\n    :reqheader Content-Type: multipart/form-data\n    :statuscode 200: successfully decrypted request\n    :statuscode 415: invalid or no certificate supplied\n    :statuscode 501: impossible to serve the request because we don't have the matching key\n    \"\"\"\n    if 'file' not in request.files:\n        return abort(415, 'no file uploaded in request data')\n\n    encrypted_payload = request.files['file'].stream.read()\n\n    try:\n        # TODO: Identify the specific certificate used to generate the request\n        encrypt_cert: EncryptionCertificate = db.session.query(EncryptionCertificate).first()\n    except NoResultFound:\n        return abort(500, 'unable to decrypt, there was no decryption cert')\n\n    pk = encrypt_cert.rsa_private_key.to_crypto()\n\n    try:\n        result = decrypt_mdmcert(encrypted_payload, pk)\n    except ValueError as e:\n        raise JSONAPIError(\n            title=\"Unable to decrypt signed request\",\n            status=415,\n            detail=\"Could not find a suitable private key to decrypt the given request\",\n        )\n\n    return result, 200, {\n        'Content-Type': 'application/octet-stream',\n        'Content-Disposition': 'attachment; filename=mdm_signed_request.%s.plist.b64' % datetime.now().strftime('%Y%m%d_%H%M%S'),\n    }\n\n"
  },
  {
    "path": "commandment/apns/mdmcert.py",
    "content": "\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2017 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\"\"\"\n\nfrom typing import Dict\nfrom flask import Response\nimport json\nfrom base64 import b64encode\nimport requests\nfrom binascii import unhexlify\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import serialization, padding\nfrom cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKeyWithSerialization\nfrom commandment.dep.smime import decrypt, decrypt_smime_content\n\nMDMCERT_REQ_URL = 'https://mdmcert.download/api/v1/signrequest'\n\n# PLEASE! Do not take this key and use it for another product/project. It's\n# only for Commandment's use. If you'd like to get your own (free!) key\n# contact the mdmcert.download administrators and get your own key for your\n# own project/product.  We're trying to keep statistics on which products are\n# requesting certs (per Apple T&C). Don't force Apple's hand and\n# ruin it for everyone!\nMDMCERT_API_KEY = 'b742461ff981756ca3f924f02db5a12e1f6639a9109db047ead1814aafc058dd'\n\nCERT_REQ_TYPE = 'mdmcert.pushcert'\n\n\ndef submit_mdmcert_request(email: str, csr_pem: str,\n                           encrypt_with_pem: str, api_key: str = MDMCERT_API_KEY) -> Dict:\n    \"\"\"Submit a CSR signing request to mdmcert.download.\n\n    Note: Need to ``export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES`` on High Sierra +\n\n    Example Response:\n\n        {'reason': 'Invalid email address. Have you registered yet at https://mdmcert.download/?', 'result': 'failure'}\n\n        on success:\n\n        {'result': 'success'}\n\n    Args:\n          email (str): Your registered mdmcert.download e-mail address.\n          api_key (str): Your registered mdmcert.download API key.\n          csr_pem (str): The MDM CSR to sign.\n          encrypt_with_pem (str): The certificate which will be used to encrypt the response.\n\n    Returns:\n          dict: Response from the mdmcert.download service.\n    \"\"\"\n    base64_csr = b64encode(csr_pem)\n    base64_recipient = b64encode(encrypt_with_pem)\n\n    mdmcert_dict = {\n        'csr': base64_csr.decode('utf8'),\n        'email': email,\n        'key': api_key,\n        'encrypt': base64_recipient.decode('utf8'),\n    }\n\n    session = requests.Session()\n\n    # This was necessary because i had Charles proxy on macOS which caused the subprocess to abort trap 6. The reason\n    # is interlinked with request's ability to read system proxy settings.\n    session.trust_env = False  # Don't read proxy settings from OS.\n\n    res = session.post(\n        MDMCERT_REQ_URL,\n        data=json.dumps(mdmcert_dict).encode('utf8'),\n        headers={\n            'Content-Type': 'application/json',\n            'User-Agent': 'coMmanDMent/0.1',\n        })\n\n    return res.json()\n\n\nclass FixedLocationResponse(Response):\n    # override Werkzeug default behaviour of \"fixing up\" once-non-compliant\n    # relative location headers. now permitted in rfc7231 sect. 7.1.2\n    autocorrect_location_header = False\n\n\ndef decrypt_mdmcert(response: bytes, decrypt_with: RSAPrivateKeyWithSerialization) -> bytes:\n    \"\"\"Decrypt a .plist.b64.p7 supplied by mdmcert.download.\n\n    In order to decrypt this we need to:\n    - decode the payload using unhexlify()\n    - find the private key that corresponded to the request.\n\n    Args:\n        response (bytes): The still encryped and hex encoded payload\n        decrypt_with (RSAPrivateKeyWithSerialization): The private key that should be used to decrypt the payload.\n\n    Returns:\n        bytes - the decrypted response\n    \"\"\"\n    decoded_payload = unhexlify(response)\n\n    # try:\n    result = decrypt_smime_content(decoded_payload, decrypt_with)\n    # except ValueError as e:\n    #     return abort(400, e)\n    # result = decrypt_with.decrypt(\n    #     decoded_payload,\n    #     padding.PKCS7(block_size=8)\n    # )\n    return result\n"
  },
  {
    "path": "commandment/apns/push.py",
    "content": "\"\"\"\nCopyright (c) 2015 Jesse Peterson\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\nAttributes:\n    apns_cxns (dict): A dictionary containing APNS connections keyed by the push certificate topic.\n\"\"\"\n\nimport os\nimport apns2\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.backends import default_backend\nfrom oscrypto.keys import parse_pkcs12\nfrom flask import g, current_app\nfrom commandment.models import Device\nimport json\n\n\ndef get_apns() -> apns2.APNSClient:\n    apns = getattr(g, '_apns', None)\n    \n    if apns is None:\n        push_certificate_path = current_app.config['PUSH_CERTIFICATE']\n        if not os.path.exists(push_certificate_path):\n            raise RuntimeError('You specified a push certificate at: {}, but it does not exist.'.format(push_certificate_path))\n\n        client_cert = push_certificate_path  # can be a single path or tuple of 2\n\n        # We can handle loading PKCS#12 but APNS2Client specifically requests PEM encoded certificates\n        push_certificate_basename, ext = os.path.splitext(push_certificate_path)\n        if ext.lower() == '.p12':\n            pem_key_path = push_certificate_basename + '.key'\n            pem_certificate_path = push_certificate_basename + '.crt'\n\n            if not os.path.exists(pem_key_path) or not os.path.exists(pem_certificate_path):\n                current_app.logger.info('You provided a PKCS#12 push certificate, we will have to encode it as PEM to continue...')\n                current_app.logger.info('.key and .crt files will be saved in the same location')\n\n                with open(push_certificate_path, 'rb') as fd:\n                    if 'PUSH_CERTIFICATE_PASSWORD' in current_app.config:\n                        key, certificate, intermediates = parse_pkcs12(fd.read(), bytes(current_app.config['PUSH_CERTIFICATE_PASSWORD'], 'utf8'))\n                    else:\n                        key, certificate, intermediates = parse_pkcs12(fd.read())\n\n                crypto_key = serialization.load_der_private_key(key.dump(), None, default_backend())\n                with open(pem_key_path, 'wb') as fd:\n                    fd.write(crypto_key.private_bytes(\n                        encoding=serialization.Encoding.PEM,\n                        format=serialization.PrivateFormat.PKCS8,\n                        encryption_algorithm=serialization.NoEncryption()))\n\n                crypto_cert = x509.load_der_x509_certificate(certificate.dump(), default_backend())\n                with open(pem_certificate_path, 'wb') as fd:\n                    fd.write(crypto_cert.public_bytes(serialization.Encoding.PEM))\n\n            client_cert = pem_certificate_path, pem_key_path\n        \n        try:\n            apns = g._apns = apns2.APNSClient(mode='prod', client_cert=client_cert)\n        except:\n            raise RuntimeError('Your push certificate is expired or invalid')\n\n    return apns\n\n\nclass MDMPayload(apns2.Payload):\n    \"\"\"A class representing an MDM APNs message payload.\"\"\"\n    def __init__(self, push_magic: str) -> None:\n        \"\"\"Constructor\n        \n            Args:\n                push_magic (str): The push magic token that was supplied by an enrolled device.\n        \"\"\"\n        super(MDMPayload, self).__init__(custom={'mdm': push_magic})\n        self._push_magic = push_magic\n\n    def to_json(self) -> str:\n        return json.dumps({'mdm': self._push_magic})\n\n\ndef push_to_device(device: Device) -> apns2.Response:\n    \"\"\"Issue a `Blank Push` to a device.\n    \n    If the push token is invalid then it will be automatically set to None\n    \n    Args:\n        device (Device): The device model to push to, must have a valid apns token and push magic\n\n    Raises:\n        ssl.SSLError [SSL: SSLV3_ALERT_CERTIFICATE_EXPIRED] sslv3 alert certificate expired (_ssl.c:777) if the push\n            certificate has expired and the system attempts a push.\n\n    Returns:\n        APNS2Client Response object\n    \"\"\"\n    current_app.logger.debug('Sending a push notification to {} on topic {}, using push magic: {}'.format(\n        device.hex_token, device.topic, device.push_magic\n    ))\n    client = get_apns()\n    payload = MDMPayload(device.push_magic)\n    notification = apns2.Notification(payload, priority=apns2.PRIORITY_LOW)\n    response: apns2.response.Response = client.push(notification, device.hex_token, device.topic)\n\n    # 410 means that the token is no longer valid for this device, so don't attempt to push any more\n    if response.status_code == 410:\n        device.token = None\n        device.push_magic = None\n\n    return response\n"
  },
  {
    "path": "commandment/apns/schema.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PushResponseFlatSchema(Schema):\n    \"\"\"This structure mimics the fields of an APNS2 service reply.\"\"\"\n    apns_id = fields.Integer()\n    status_code = fields.Integer()\n    reason = fields.Str()\n    timestamp = fields.DateTime()\n\n"
  },
  {
    "path": "commandment/apns/threads.py",
    "content": "from typing import Tuple\nimport logging\nimport threading\nfrom datetime import datetime\nimport dateutil.parser\nfrom flask import Flask\nimport ssl\n\nfrom commandment.mdm import CommandStatus\nfrom commandment.models import db, Device, Command\nfrom commandment.apns.push import push_to_device\nimport sqlalchemy.orm.exc\nfrom sqlalchemy import func\n\npush_thread = None\npush_start = 2\npush_time = 90\npush_thread_stopped = threading.Event()\n\nlogger = logging.getLogger('push thread')\n\n\ndef start(app: Flask):\n    \"\"\"Start the APNS Pusher thread\"\"\"\n\n    logger.info('PUSH thread will start in %d second(s). polling at intervals of %d second(s).', push_start, push_time)\n    push_thread = threading.Timer(push_start, push_thread_callback, [app])\n    push_thread.daemon = True\n    push_thread.start()\n\n\ndef stop():\n    \"\"\"Stop the APNS Pusher thread\"\"\"\n    logger.info('PUSH thread will stop')\n    push_thread_stopped.set()\n\n    global push_thread\n    if push_thread is threading.Timer:\n        push_thread.cancel()\n\n\ndef push_thread_callback(app: Flask):\n    \"\"\"Process outstanding MDM commands by issuing a push to device(s).\n\n    TODO: A push with no response needs an exponential backoff time.\n\n    Commands that are ready to send must satisfy these criteria:\n\n    - Command is in Queued state.\n    - Command.after is null.\n    - Command.ttl is not zero.\n    - Device is enrolled (is_enrolled)\n    \"\"\"\n    while not push_thread_stopped.wait(push_time):\n        app.logger.info('Push Thread checking for outstanding commands...')\n        with app.app_context():\n            pending: Tuple[Device, int] = db.session.query(Device, func.Count(Command.id)).\\\n                filter(Device.id == Command.device_id).\\\n                filter(Command.status == CommandStatus.Queued).\\\n                filter(Command.ttl > 0).\\\n                filter(Command.after == None).\\\n                filter(Device.is_enrolled == True).\\\n                group_by(Device.id).\\\n                all()\n\n            for d, c in pending:\n                app.logger.info('PENDING: %d command(s) for device UDID %s', c, d.udid)\n\n                if d.token is None or d.push_magic is None:\n                    app.logger.warn('Cannot request push on a device that has no device token or push magic')\n                    continue\n\n                try:\n                    response = push_to_device(d)\n                except ssl.SSLError:\n                    return stop()\n\n                app.logger.info(\"[APNS2 Response] Status: %d, Reason: %s, APNS ID: %s, Timestamp\",\n                                        response.status_code, response.reason, response.apns_id.decode('utf-8'))\n                d.last_push_at = datetime.utcnow()\n                if response.status_code == 200:\n                    d.last_apns_id = response.apns_id\n\n            db.session.commit()\n"
  },
  {
    "path": "commandment/app.py",
    "content": "from commandment import create_app\n\napp = create_app(None)\n"
  },
  {
    "path": "commandment/apps/__init__.py",
    "content": "from enum import Enum\n\n\nclass ManagedAppStatus(Enum):\n    \"\"\"A list of possible Managed Application statuses returned by the `ManagedApplicationList` command.\"\"\"\n    NeedsRedemption = 'NeedsRedemption'\n    Redeeming = 'Redeeming'\n    Prompting = 'Prompting'\n    PromptingForLogin = 'PromptingForLogin'\n    Installing = 'Installing'\n    ValidatingPurchase = 'ValidatingPurchase'\n    Managed = 'Managed'\n    ManagedButUninstalled = 'ManagedButUninstalled'\n    PromptingForUpdate = 'PromptingForUpdate'\n    PromptingForUpdateLogin = 'PromptingForUpdateLogin'\n    PromptingForManagement = 'PromptingForManagement'\n    Updating = 'Updating'\n    ValidatingUpdate = 'ValidatingUpdate'\n    Unknown = 'Unknown'\n\n    # Transient\n    UserInstalledApp = 'UserInstalledApp'\n    UserRejected = 'UserRejected'\n    UpdateRejected = 'UpdateRejected'\n    ManagementRejected = 'ManagementRejected'\n    Failed = 'Failed'\n\n    # Commandment ONLY - To indicate that the command for IA is queued but not yet acked\n    Queued = 'Queued'\n"
  },
  {
    "path": "commandment/apps/app_jsonapi.py",
    "content": "from flask import Blueprint, request\nfrom flask_rest_jsonapi import Api\n\nfrom commandment.apps.resources import ApplicationDetail, ApplicationList, ApplicationRelationship, \\\n    ManagedApplicationList, ManagedApplicationDetail, ManagedApplicationRelationship, MASApplicationList, \\\n    MASApplicationDetail, IOSApplicationList, IOSApplicationDetail\n\napi_app = Blueprint('applications_api', __name__)\napi = Api(blueprint=api_app)\n\napi.route(ApplicationList, 'applications_list',\n          '/v1/applications')\napi.route(ApplicationDetail, 'application_detail',\n          '/v1/applications/<int:application_id>')\napi.route(ApplicationRelationship, 'application_tags', '/v1/applications/<int:application_id>/relationships/tags')\n\napi.route(ManagedApplicationList, 'managed_applications_list',\n          '/v1/managed_applications', '/v1/applications/<int:application_id>/managed_applications')\napi.route(ManagedApplicationDetail, 'managed_application_detail',\n          '/v1/managed_applications/<int:managed_application_id>')\napi.route(ManagedApplicationRelationship, 'managed_application_device',\n          '/v1/managed_applications/<int:application_id>/relationships/device')\n\n# Platform specific subclasses\n\napi.route(MASApplicationList, 'mas_applications_list',\n          '/v1/applications/store/mac')\napi.route(MASApplicationDetail, 'mas_application_detail',\n          '/v1/applications/store/mac/<int:application_id>')\n\napi.route(IOSApplicationList, 'ios_applications_list',\n          '/v1/applications/store/ios')\napi.route(IOSApplicationDetail, 'ios_application_detail',\n          '/v1/applications/store/ios/<int:application_id>')\n\n"
  },
  {
    "path": "commandment/apps/models.py",
    "content": "from enum import Enum, IntEnum, IntFlag\n\nfrom commandment.apps import ManagedAppStatus\nfrom ..models import db\n\n\nclass ManagementFlag(IntFlag):\n    \"\"\"This enum of integer bitwise OR flags represents all the fields available as part of the ``ManagementFlag``\n    option to the ``InstallApplication`` command.\"\"\"\n    NOTHING = 0\n    REMOVE_APP_WITH_ENROLLMENT = 1\n    PREVENT_APPDATA_BACKUP = 4\n\n\nclass PurchaseMethod(IntEnum):\n    \"\"\"Purchase methods, the flag should almost always be VPP_APP_ASSIGNMENT\"\"\"\n    LEGACY_VPP = 0\n    VPP_APP_ASSIGNMENT = 1\n\n\nclass ApplicationType(Enum):\n    \"\"\"A list of the polymorphic identities available for subclasses of Application.\"\"\"\n    ENTERPRISE_MAC = 'enterprise_mac'\n    ENTERPRISE_IOS = 'enterprise_ios'\n    APPSTORE_MAC = 'appstore_mac'\n    APPSTORE_IOS = 'appstore_ios'\n\n\napplication_tags = db.Table(\n    'application_tags',\n    db.metadata,\n    db.Column('application_id', db.Integer, db.ForeignKey('applications.id')),\n    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')),\n)\n\n\nclass Application(db.Model):\n    \"\"\"This table holds details of each individual application that may be\n     managed (either app store or enterprise application).\n\n    :table: applications\n    \"\"\"\n    __tablename__ = 'applications'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (db.Integer): ID\"\"\"\n    display_name = db.Column(db.String, nullable=False)\n    \"\"\"display_name (db.String): The name of the application displayed in the MDM.\"\"\"\n    description = db.Column(db.String)\n    \"\"\"description (db.String): Description of this application, possibly including release notes.\"\"\"\n    version = db.Column(db.String)\n    \"\"\"version (db.String): Application version.\"\"\"\n    itunes_store_id = db.Column(db.Integer)\n    \"\"\"itunes_store_id (db.Integer): The application’s iTunes Store ID.\"\"\"\n    bundle_id = db.Column(db.String, index=True, nullable=False)\n    \"\"\"bundle_id (db.String): The application bundle identifier.\"\"\"\n    purchase_method = db.Column(db.Enum(PurchaseMethod))\n    \"\"\"purchase_method (db.Integer): Used in the Options key of InstallApplication to denote the purchase method.\"\"\"\n    manifest_url = db.Column(db.String)\n    \"\"\"manifest_url (db.String): The application manifest URL if iTunesStoreID is not supplied (an enterprise app).\"\"\"\n    management_flags = db.Column(db.Integer)\n    \"\"\"management_flags (ManagementFlag): Denotes whether app is removed with MDM profile, and whether the user may back\n        up application data.\"\"\"\n    change_management_state = db.Column(db.String, default=\"Managed\")\n    \"\"\"change_management_state (db.String): Take ownership of an existing application that is unmanaged.\"\"\"\n    discriminator = db.Column(db.String(20))\n    \"\"\"discriminator (str): The type of application\"\"\"\n\n    # iTunes Search API - Cached Result\n    country = db.Column(db.String(2))\n    \"\"\"country (str): The two letter country code of the store country. We cache this to avoid assigning apps to devices\n        that cannot even install them due to the Apple ID residing in a different locale.\"\"\"\n\n    artist_id = db.Column(db.Integer)\n    \"\"\"artist_id (int): The iTunes Artist ID, which is commonly the developer in the app store.\"\"\"\n    artist_name = db.Column(db.String)\n    \"\"\"artist_id (str): The iTunes Artist Name, which is commonly the developer in the app store.\"\"\"\n    artist_view_url = db.Column(db.String)\n    artwork_url60 = db.Column(db.String)\n    \"\"\"artwork_url60 (str): A URL to the 60x60 icon for this result.\"\"\"\n    artwork_url100 = db.Column(db.String)\n    \"\"\"artwork_url100 (str): A URL to the 100x100 icon for this result.\"\"\"\n    artwork_url512 = db.Column(db.String)\n    \"\"\"artwork_url512 (str): A URL to the 512x512 icon for this result.\"\"\"\n    release_notes = db.Column(db.String)\n    release_date = db.Column(db.DateTime)\n    minimum_os_version = db.Column(db.String)\n    file_size_bytes = db.Column(db.BigInteger)\n\n    tags = db.relationship(\n        'Tag',\n        secondary=application_tags,\n        backref='applications'\n    )\n\n    __mapper_args__ = {\n        'polymorphic_on': discriminator,\n        'polymorphic_identity': 'applications',\n    }\n\n\nclass EnterpriseMacApplication(Application):\n    \"\"\"Polymorphic single table inheritance specifically for Enterprise Mac Applications.\n\n    These applications are .pkg files which are often distributed by the MDM or from a host outside of the App Store.\n    \"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': ApplicationType.ENTERPRISE_MAC.value\n    }\n\n\nclass EnterpriseiOSApplication(Application):\n    \"\"\"Polymorphic single table inheritance specifically for Enterprise iOS Applications.\n\n    These applications are .ipa files which are often distributed by the MDM or from a host outside of the App Store.\n    With or without provisioning profiles.\n    \"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': ApplicationType.ENTERPRISE_IOS.value\n    }\n\n\nclass AppstoreMacApplication(Application):\n    \"\"\"Polymorphic single table inheritance specifically for MAS (App Store) Mac Applications.\n\n    These applications are distributed by VPP using an iTunes Store ID\n    \"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': ApplicationType.APPSTORE_MAC.value\n    }\n\n\nclass AppstoreiOSApplication(Application):\n    \"\"\"Polymorphic single table inheritance specifically for App Store iOS Applications.\n\n    These applications are distributed by VPP using an iTunes Store ID\n    \"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': ApplicationType.APPSTORE_IOS.value\n    }\n\n\nclass ApplicationManifest(db.Model):\n    \"\"\"An application manifest describes a non-App store installable application.\n\n    See: `macOS Application <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html#//apple_ref/doc/uid/TP40017387-CH3-SW755>`_.\n\n    :table: application_manifests\n    \"\"\"\n    __tablename__ = 'application_manifests'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (db.Integer): ID\"\"\"\n    bundle_id = db.Column(db.String, index=True, nullable=False)\n    \"\"\"bundle_id (db.String): Bundle Identifier of the top-level distribution package.\"\"\"\n    bundle_version = db.Column(db.String, index=True)\n    \"\"\"bundle_version (db.String): Bundle Version of the top-level distribution package.\"\"\"\n    kind = db.Column(db.String, default='software')\n    \"\"\"kind (db.String): Type of item to install, at the moment ignored and always set to 'software'.\"\"\"\n    size_in_bytes = db.Column(db.BigInteger)\n    \"\"\"size_in_bytes (db.BigInteger): Size of the package (in bytes).\"\"\"\n    subtitle = db.Column(db.String)\n    \"\"\"subtitle (db.String):\"\"\"\n    title = db.Column(db.String)\n    \"\"\"title (db.String):\"\"\"\n    full_size_image_url = db.Column(db.String)\n    \"\"\"full_size_image_url (db.String): URL to full size image. may be null\"\"\"\n    full_size_image_needs_shine = db.Column(db.Boolean, default=False)\n    \"\"\"full_size_image_needs_shine (db.Boolean): Whether the image needs the shine effect placed over it.\"\"\"\n    display_image_url = db.Column(db.String)\n    \"\"\"display_image_url (db.String): URL to display image. may be null\"\"\"\n    display_image_needs_shine = db.Column(db.Boolean, default=False)\n    \"\"\"display_image_needs_shine (db.Boolean): Whether the display image needs the shine effect placed over it.\"\"\"\n    checksums = db.relationship('ApplicationManifestChecksum', back_populates='application_manifest')\n\n\nclass ApplicationManifestChecksum(db.Model):\n    __tablename__ = 'application_manifest_checksums'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (db.Integer): ID\"\"\"\n    application_manifest_id = db.Column(db.Integer, db.ForeignKey('application_manifests.id'))\n    \"\"\"application_manifest_id (db.Integer): Foreign key reference to the parent manifest.\"\"\"\n    application_manifest = db.relationship(ApplicationManifest, back_populates='checksums')\n    \"\"\"application_manifest (db.relationship): Relationship to the parent manifest.\"\"\"\n    checksum_index = db.Column(db.Integer, nullable=False)\n    \"\"\"checksum_index (db.Integer): Index of this checksum in the sequence of checksums.\"\"\"\n    checksum_value = db.Column(db.String(32), nullable=False)\n    \"\"\"checksum_value (db.String): 32 byte MD5 checksum of this chunk. Chunk size is defined as 10485760 bytes (10mb)\"\"\"\n\n\nclass AppSourceType(Enum):\n    S3 = 'S3'\n    Munki = 'Munki'\n\n\nclass ApplicationSource(db.Model):\n    \"\"\"This table holds rows indicating sources that may referenced in ``InstallApplication`` commands.\n\n    The MDM may require write access to create application manifests from existing items.\n\n    :table: application_sources\n    \"\"\"\n    __tablename__ = 'application_sources'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (db.Integer): ID\"\"\"\n    name = db.Column(db.String)\n    \"\"\"name (db.String): A short, descriptive name for the source. Only used in display.\"\"\"\n    source_type = db.Column(db.Enum(AppSourceType), default=AppSourceType.Munki)\n    \"\"\"source_type (AppSourceType): The application source type.\"\"\"\n\n    endpoint = db.Column(db.String)\n    \"\"\"endpoint (db.String): The hostname for object storage or URI for read-only munki repositories.\"\"\"\n    mount_uri = db.Column(db.String)\n    \"\"\"mount_uri (db.String): The R/W mount URI for munki repositories only.\"\"\"\n    use_ssl = db.Column(db.Boolean)\n    \"\"\"use_ssl (Boolean): Use SSL when connecting to endpoint. Used when endpoint is host only.\"\"\"\n\n    # For S3 / Minio\n    access_key = db.Column(db.String)\n    \"\"\"access_key (db.String): The access key for S3 / Minio that uniquely identifies this client.\"\"\"\n    secret_key = db.Column(db.String)\n    \"\"\"secret_key (db.String): The secret key for S3 / Minio that authenticates this client.\"\"\"\n    bucket = db.Column(db.String)\n    \"\"\"bucket (db.String): The bucket name that holds installation packages.\"\"\"\n\n\nclass ManagedApplication(db.Model):\n    \"\"\"This table holds rows for application installation statuses that are reported by the `ManagedApplicationList`\n    command.\"\"\"\n    __tablename__ = 'managed_applications'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (db.Integer): ID\"\"\"\n    device_id = db.Column(db.ForeignKey('devices.id'))\n    \"\"\"(int): Device foreign key ID.\"\"\"\n    device = db.relationship('Device', backref='managed_applications')\n    \"\"\"(db.relationship): Device relationship\"\"\"\n    bundle_id = db.Column(db.String)\n    \"\"\"(db.String): The bundle identifier of the application being installed.\"\"\"\n    external_version_id = db.Column(db.Integer)\n    \"\"\"(db.Integer): The external version identifier (which is also shown in the vpp contentMetadataUrl lookup)\"\"\"\n    has_configuration = db.Column(db.Boolean)\n    \"\"\"(db.Boolean): Whether the app has managed app configuration or not.\"\"\"\n    has_feedback = db.Column(db.Boolean)\n    \"\"\"(db.Boolean): Whether the app has managed app feedback or not.\"\"\"\n    is_validated = db.Column(db.Boolean)\n    \"\"\"(db.Boolean): Whether the app has been validated.\"\"\"\n    management_flags = db.Column(db.Integer)\n    \"\"\"(db.Integer): Which management flags the application has been installed with.\"\"\"\n    status = db.Column(db.Enum(ManagedAppStatus))\n    \"\"\"(ManagedAppStatus): The status of the managed application.\"\"\"\n    application_id = db.Column(db.ForeignKey('applications.id'), nullable=True)\n    \"\"\"(db.ForeignKey): Foreign key reference to the application row which was assigned to the device\"\"\"\n    application = db.relationship('Application', backref='managed_applications')\n    \"\"\"(db.relationship): relationship to the defined application object.\"\"\"\n    ia_command_id = db.Column(db.ForeignKey('commands.id'), nullable=True)\n    \"\"\"(db.ForeignKey): Foreign key reference to the last `InstallApplication` command that \n        installed this app on the device.\"\"\"\n    ia_command = db.relationship('Command', backref='managed_application')\n    \"\"\"(db.relationship): Relationship to the last command that was sent in regards to this application entry\"\"\"\n"
  },
  {
    "path": "commandment/apps/resources.py",
    "content": "from sqlalchemy.orm.exc import NoResultFound\nfrom flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship\nfrom flask_rest_jsonapi.exceptions import ObjectNotFound\nfrom commandment.apps.schema import ApplicationManifestSchema, ApplicationSchema, ManagedApplicationSchema\nfrom commandment.apps.models import db, ApplicationManifest, Application, ManagedApplication, AppstoreMacApplication, \\\n    AppstoreiOSApplication, EnterpriseMacApplication, EnterpriseiOSApplication\n\n\nclass ApplicationManifestDetail(ResourceDetail):\n    schema = ApplicationManifestSchema\n    data_layer = {\n        'session': db.session,\n        'model': ApplicationManifest,\n        'url_field': 'application_manifest_id'\n    }\n\n\nclass ApplicationDetail(ResourceDetail):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': Application,\n        'url_field': 'application_id'\n    }\n\n\nclass ApplicationList(ResourceList):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': Application,\n        'url_field': 'application_id'\n    }\n\n\nclass ApplicationRelationship(ResourceRelationship):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': Application,\n        'url_field': 'application_id'\n    }\n\n\nclass MASApplicationDetail(ResourceDetail):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': AppstoreMacApplication,\n        'url_field': 'application_id'\n    }\n\n\nclass MASApplicationList(ResourceList):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': AppstoreMacApplication,\n        'url_field': 'application_id'\n    }\n\n\nclass IOSApplicationDetail(ResourceDetail):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': AppstoreiOSApplication,\n        'url_field': 'application_id'\n    }\n\n\nclass IOSApplicationList(ResourceList):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': AppstoreiOSApplication,\n        'url_field': 'application_id'\n    }\n\n\nclass EnterpriseMacApplicationList(ResourceList):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': EnterpriseMacApplication,\n        'url_field': 'application_id'\n    }\n\n\nclass EnterpriseMacApplicationDetail(ResourceDetail):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': EnterpriseMacApplication,\n        'url_field': 'application_id'\n    }\n\n\nclass EnterpriseIosApplicationList(ResourceList):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': EnterpriseiOSApplication,\n        'url_field': 'application_id'\n    }\n\n\nclass EnterpriseIosApplicationDetail(ResourceDetail):\n    schema = ApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': EnterpriseiOSApplication,\n        'url_field': 'application_id'\n    }\n\n\nclass ManagedApplicationDetail(ResourceDetail):\n    schema = ManagedApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': ManagedApplication,\n        'url_field': 'managed_application_id',\n    }\n\n\nclass ManagedApplicationList(ResourceList):\n    def query(self, view_kwargs):\n        query_ = self.session.query(ManagedApplication)\n        if view_kwargs.get('application_id') is not None:\n            try:\n                self.session.query(Application).filter_by(id=view_kwargs['application_id']).one()\n            except NoResultFound:\n                raise ObjectNotFound({'parameter': 'application_id'},\n                                     \"Application: {} not found\".format(view_kwargs['application_id']))\n            else:\n                query_ = query_.join(Application).filter(Application.id == view_kwargs['application_id'])\n        return query_\n\n    schema = ManagedApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': ManagedApplication,\n        'url_field': 'managed_application_id',\n        'methods': {'query': query},\n    }\n\n\nclass ManagedApplicationRelationship(ResourceRelationship):\n    schema = ManagedApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': ManagedApplication,\n        'url_field': 'managed_application_id',\n    }\n"
  },
  {
    "path": "commandment/apps/schema.py",
    "content": "from marshmallow_jsonapi import fields\nfrom marshmallow_jsonapi.flask import Relationship, Schema\n\n\nclass ApplicationSchema(Schema):\n    class Meta:\n        type_ = 'applications'\n        self_view = 'applications_api.application_detail'\n        self_view_kwargs = {'application_id': '<id>'}\n        self_view_many = 'applications_api.applications_list'\n        strict = True\n\n    id = fields.Int(dump_only=True)\n    display_name = fields.Str()\n    description = fields.Str()\n    version = fields.Str()\n    itunes_store_id = fields.Int()\n    bundle_id = fields.Str()\n    purchase_method = fields.Int()\n    manifest_url = fields.Url()\n    management_flags = fields.Int()\n    change_management_state = fields.Str()\n\n    # iTunes Search API cache\n    country = fields.Str()\n    artist_id = fields.Int()\n    artist_name = fields.Str()\n    artist_view_url = fields.Url()\n    artwork_url60 = fields.Url()\n    artwork_url100 = fields.Url()\n    artwork_url512 = fields.Url()\n    release_notes = fields.Str()\n    release_date = fields.DateTime()\n    minimum_os_version = fields.Str()\n    file_size_bytes = fields.Number()\n\n    # expose the underlying polymorphic identity for lists that contain all types of apps\n    discriminator = fields.Str()\n\n    tags = Relationship(\n        related_view='api_app.tags_list',\n        related_view_kwargs={'application_id': '<id>'},\n        many=True,\n        schema='TagSchema',\n        type_='tags'\n    )\n\n\nclass ManagedApplicationSchema(Schema):\n    class Meta:\n        type_ = 'managed_applications'\n        self_view = 'applications_api.managed_application_detail'\n        self_view_kwargs = {'managed_application_id': '<id>'}\n        self_view_many = 'applications_api.managed_applications_list'\n\n    id = fields.Int(dump_only=True)\n    bundle_id = fields.Str()\n    external_version_id = fields.Int()\n    has_configuration = fields.Bool()\n    has_feedback = fields.Bool()\n    is_validated = fields.Bool()\n    management_flags = fields.Int()\n    status = fields.Str()\n\n    device = Relationship(\n        related_view='api_app.device_detail',\n        related_view_kwargs={'device_id': '<device_id>'},\n        many=False,\n        schema='DeviceSchema',\n        type_='devices',\n    )\n\n\nclass ApplicationManifestSchema(Schema):\n    class Meta:\n        type_ = 'application_manifests'\n        self_view = 'applications_api.application_manifest_detail'\n        self_view_kwargs = {'application_manifest_id': '<id>'}\n        self_view_many = 'applications_api.application_manifest_list'\n        strict = True\n\n    checksums = Relationship(\n        related_view='applications_api.application_manifest_checksum_detail',\n        related_view_kwargs={'application_checksum_id': '<id>'},\n        many=True,\n        schema='ApplicationManifestChecksumSchema',\n        type_='application_manifest_checksums'\n    )\n\n    full_size_image_url = fields.Url()\n    display_image_url = fields.Url()\n\n\nclass ApplicationManifestChecksumSchema(Schema):\n    class Meta:\n        type_ = 'application_manifest_checksums'\n        self_view = 'applications_api.application_manifest_checksum_detail'\n        self_view_kwargs = {'application_checksum_id': '<id>'}\n        self_view_many = 'applications_api.application_manifest_checksum_list'\n        strict = True\n\n"
  },
  {
    "path": "commandment/auth/__init__.py",
    "content": ""
  },
  {
    "path": "commandment/auth/app.py",
    "content": "from flask import Blueprint, request\nfrom .oauth2 import authorization\n\noauth_app = Blueprint('oauth_app', __name__)\n\n# @bp.route('/authorize', methods=['GET', 'POST'])\n# def authorize():\n#     if current_user:\n#         form = ConfirmForm()\n#     else:\n#         form = LoginConfirmForm()\n#\n#     if form.validate_on_submit():\n#         if form.confirm.data:\n#             # granted by current user\n#             grant_user = current_user\n#         else:\n#             grant_user = None\n#         return authorization.create_authorization_response(grant_user)\n#     try:\n#         grant = authorization.validate_authorization_request()\n#     except OAuth2Error as error:\n#         # TODO: add an error page\n#         payload = dict(error.get_body())\n#         return jsonify(payload), error.status_code\n#\n#     client = OAuth2Client.get_by_client_id(request.args['client_id'])\n#     return render_template(\n#         'account/authorize.html',\n#         grant=grant,\n#         scopes=scopes,\n#         client=client,\n#         form=form,\n#     )\n\n\n@oauth_app.route('/token', methods=['POST'])\ndef issue_token():\n    return authorization.create_token_response(request=request)\n\n\n@oauth_app.route('/revoke', methods=['POST'])\ndef revoke_token():\n    return authorization.create_revocation_response()\n"
  },
  {
    "path": "commandment/auth/models.py",
    "content": "from commandment.models import db\nfrom authlib.flask.oauth2.sqla import OAuth2ClientMixin, OAuth2TokenMixin\n\n\nclass User(db.Model):\n    __tablename__ = 'users'\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String)\n    fullname = db.Column(db.String)\n    password = db.Column(db.String)\n\n    def get_user_id(self):\n        \"\"\"This method is implemented as part of the Resource Owner interface for Authlib.\"\"\"\n        return self.id\n\n\nclass OAuth2Client(db.Model, OAuth2ClientMixin):\n    \"\"\"OAuth 2 Client\"\"\"\n    __tablename__ = 'oauth2_clients'\n\n    id = db.Column(db.Integer, primary_key=True)\n    user_id = db.Column(\n        db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')\n    )\n    user = db.relationship('User')\n\n\nclass OAuth2Token(db.Model, OAuth2TokenMixin):\n    \"\"\"Bearer Token\"\"\"\n    __tablename__ = 'oauth2_tokens'\n\n    id = db.Column(db.Integer, primary_key=True)\n    user_id = db.Column(\n        db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')\n    )\n    user = db.relationship('User')\n\n"
  },
  {
    "path": "commandment/auth/oauth2.py",
    "content": "from flask import current_app\nfrom authlib.flask.oauth2 import (\n    AuthorizationServer,\n    ResourceProtector,\n)\nfrom authlib.flask.oauth2.sqla import (\n    create_query_token_func,\n    create_save_token_func,\n    create_query_client_func,\n)\nfrom authlib.specs.rfc6749.grants import (\n    AuthorizationCodeGrant as _AuthorizationCodeGrant,\n    ImplicitGrant as _ImplicitGrant,\n    ResourceOwnerPasswordCredentialsGrant as _PasswordGrant,\n    ClientCredentialsGrant as _ClientCredentialsGrant,\n    RefreshTokenGrant as _RefreshTokenGrant,\n)\nfrom authlib.specs.rfc7009 import RevocationEndpoint as _RevocationEndpoint\nfrom werkzeug.security import gen_salt\nfrom .models import (\n    db, User,\n    OAuth2Client,\n    # OAuth2AuthorizationCode,\n    OAuth2Token,\n)\nfrom authlib.oauth2.rfc6750 import BearerTokenValidator\nfrom authlib.oauth2 import (\n    OAuth2Error,\n)\nfrom authlib.oauth2.rfc6749 import (\n    MissingAuthorizationError,\n)\n\n#\n# class AuthorizationCodeGrant(_AuthorizationCodeGrant):\n#     def create_authorization_code(self, client, user, request):\n#         code = gen_salt(48)\n#         item = OAuth2AuthorizationCode(\n#             code=code,\n#             client_id=client.client_id,\n#             redirect_uri=request.redirect_uri,\n#             scope=request.scope,\n#             user_id=user.id,\n#         )\n#         db.session.add(item)\n#         db.session.commit()\n#         return code\n#\n#     def parse_authorization_code(self, code, client):\n#         item = OAuth2AuthorizationCode.query.filter_by(\n#             code=code, client_id=client.client_id).first()\n#         if item and not item.is_expired():\n#             return item\n#\n#     def delete_authorization_code(self, authorization_code):\n#         db.session.delete(authorization_code)\n#         db.session.commit()\n#\n#     def create_access_token(self, token, client, authorization_code):\n#         item = OAuth2Token(\n#             client_id=client.client_id,\n#             user_id=authorization_code.user_id,\n#             **token\n#         )\n#         db.session.add(item)\n#         db.session.commit()\n#         token['user_id'] = authorization_code.user_id\n\n\nclass ImplicitGrant(_ImplicitGrant):\n    def create_access_token(self, token, client, grant_user):\n        item = OAuth2Token(\n            client_id=client.client_id,\n            user_id=grant_user.id,\n            **token\n        )\n        db.session.add(item)\n        db.session.commit()\n\n\nclass PasswordGrant(_PasswordGrant):\n    def authenticate_user(self, username, password):\n        current_app.logger.info('user: %s logging in using resource owner password grant', username)\n        user = User.query.filter_by(name=username).first()\n        return user\n        # if user.check_password(password):\n        #     return user\n\n    def create_access_token(self, token, client, user):\n        item = OAuth2Token(\n            client_id=client.client_id,\n            user_id=user.id,\n            **token\n        )\n        db.session.add(item)\n        db.session.commit()\n        token['user_id'] = user.id\n\n\nclass ClientCredentialsGrant(_ClientCredentialsGrant):\n    def create_access_token(self, token, client):\n        item = OAuth2Token(\n            client_id=client.client_id,\n            user_id=client.user_id,\n            **token\n        )\n        db.session.add(item)\n        db.session.commit()\n\n\nclass RefreshTokenGrant(_RefreshTokenGrant):\n    def authenticate_refresh_token(self, refresh_token):\n        item = OAuth2Token.query.filter_by(refresh_token=refresh_token).first()\n        if item and not item.is_refresh_token_expired():\n            return item\n\n    def create_access_token(self, token, authenticated_token):\n        item = OAuth2Token(\n            client_id=authenticated_token.client_id,\n            user_id=authenticated_token.user_id,\n            **token\n        )\n        db.session.add(item)\n        db.session.delete(authenticated_token)\n        db.session.commit()\n\n\nclass RevocationEndpoint(_RevocationEndpoint):\n    def query_token(self, token, token_type_hint, client):\n        q = OAuth2Token.query.filter_by(client_id=client.client_id)\n        if token_type_hint == 'access_token':\n            return q.filter_by(access_token=token).first()\n        elif token_type_hint == 'refresh_token':\n            return q.filter_by(refresh_token=token).first()\n        # without token_type_hint\n        item = q.filter_by(access_token=token).first()\n        if item:\n            return item\n        return q.filter_by(refresh_token=token).first()\n\n    def invalidate_token(self, token):\n        db.session.delete(token)\n        db.session.commit()\n\n\nquery_client = create_query_client_func(db.session, OAuth2Client)\nsave_token = create_save_token_func(db.session, OAuth2Token)\nauthorization = AuthorizationServer(query_client=query_client, save_token=save_token)\n\n# support all grants\n# authorization.register_grant_endpoint(AuthorizationCodeGrant)\nauthorization.register_grant(ImplicitGrant)\nauthorization.register_grant(PasswordGrant)\nauthorization.register_grant(ClientCredentialsGrant)\nauthorization.register_grant(RefreshTokenGrant)\n\n# support revocation\n# authorization.register_grant(RevocationEndpoint)\n\n# scopes definition\nscopes = {\n    'email': 'Access to your email address.',\n    'connects': 'Access to your connected networks.'\n}\n\n\nclass CommandmentBearerTokenValidator(BearerTokenValidator):\n    def authenticate_token(self, token_string):\n        return OAuth2Token.query.filter_by(access_token=token_string).first()\n\n    def request_invalid(self, request):\n        return False\n\n    def token_revoked(self, token):\n        return token.revoked\n\n\nclass FlaskJSONAPIResourceProtector(ResourceProtector):\n    \"\"\"This class pretends to be the Flask-OAuthlib manager for Flask-Rest-JSONAPI\"\"\"\n    _after_request_funcs = []\n\n    def verify_request(self, scopes):\n        current_app.logger.info('verifying token against scopes: %s', scopes)\n        try:\n            # self.acquire_token(scopes)\n            self.acquire_token('')  # We are currently not checking scopes.\n        except MissingAuthorizationError as error:\n            self.raise_error_response(error)\n        except OAuth2Error as error:\n            self.raise_error_response(error)\n        return True, []\n\n\n# protect resource\nquery_token = create_query_token_func(db.session, OAuth2Token)\nrequire_oauth = FlaskJSONAPIResourceProtector()\nrequire_oauth.register_token_validator(CommandmentBearerTokenValidator())\n\n\ndef init_app(app):\n    authorization.init_app(app)\n"
  },
  {
    "path": "commandment/cli.py",
    "content": "#!/usr/bin/env python\n\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2017 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\"\"\"\n\nimport os\nfrom commandment import create_app\nfrom commandment.pki.ssl import generate_self_signed_certificate\nfrom cryptography.hazmat.primitives import serialization\n\nfrom commandment.apns.push import get_apns\n\n\ndef server():\n    \"\"\"Run server in standalone development mode.\"\"\"\n    \n    app = create_app(os.environ['COMMANDMENT_SETTINGS'])\n\n    # Werkzeug, in debug mode, will launch the app using the debug file-system\n    # watching auto-reloader. For threads this means that there would be two\n    # sets of threads launched. Here we try to guard against that by only\n    # starting our runner threads when either the reloader (debug) is off, or\n    # only in the reloader sub-process and not the reloader parent process to\n    # avoid extraneous threads being created.\n\n    # TODO: re-enable runner after python3 rewrite\n\n    with app.app_context():\n        apns = get_apns()\n    #\n    #\n    # if not app.config.get('DEBUG') or werkzeug.serving.is_running_from_reloader():\n    #     start_runner()\n    #     atexit.register(stop_runner)\n\n    cert_path = os.path.join(app.root_path, app.config.get('SSL_CERTIFICATE'))\n    key_path = os.path.join(app.root_path, app.config.get('SSL_RSA_KEY'))\n    app.logger.debug('Using RSA Private Key From: %s', os.path.abspath(key_path))\n    app.logger.debug('Using SSL Certificate From: %s', os.path.abspath(cert_path))\n\n    # pk, csr = generate_signing_request(app.config['PUBLIC_HOSTNAME'])\n    # app.logger.debug('Generated signing request for', app.config['PUBLIC_HOSTNAME'])\n\n    if not os.path.exists(cert_path) and not os.path.exists(key_path):\n        app.logger.info('Generating Self Signed Certificate')\n        pk, cert = generate_self_signed_certificate(app.config['PUBLIC_HOSTNAME'])\n\n        pem_key = pk.private_bytes(\n            encoding=serialization.Encoding.PEM,\n            format=serialization.PrivateFormat.PKCS8,\n            encryption_algorithm=serialization.NoEncryption()\n        )\n\n        with open(key_path, 'wb') as fd:\n            fd.write(pem_key)\n\n        pem_cert = cert.public_bytes(\n            encoding=serialization.Encoding.PEM\n        )\n\n        with open(cert_path, 'wb') as fd:\n            fd.write(pem_cert)\n\n\n    # http://werkzeug.pocoo.org/docs/0.11/serving/#werkzeug.serving.run_simple\n    app.run(\n        host='0.0.0.0',\n        port=app.config.get('PORT'),\n        ssl_context=(cert_path, key_path),\n        threaded=True)\n"
  },
  {
    "path": "commandment/cms/__init__.py",
    "content": "from typing import Union, Optional, Type\nfrom asn1crypto.cms import CertificateSet, SignerIdentifier, Certificate, SignedDigestAlgorithm, DigestAlgorithm\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import padding\n\n\ndef _certificate_by_signer_identifier(certificates: CertificateSet, sid: SignerIdentifier) -> Optional[Certificate]:\n    \"\"\"Find a signer certificate by its SignerIdentifier.\n\n    Args:\n          certificates (CertificateSet): Set of certificates parsed by asn1crypto.\n          sid (SignerIdentifier): Signer Identifier, usually IssuerAndSerialNumber.\n    Returns:\n          cms.Certificate or None\n    \"\"\"\n    if sid.name != 'issuer_and_serial_number':\n        return None  # Only IssuerAndSerialNumber for now\n\n    #: IssuerAndSerialNumber\n    ias = sid.chosen\n\n    for c in certificates:\n        if c.name != 'certificate':\n            continue  # we only support certificate for now\n\n        chosen = c.chosen  #: Certificate\n\n        if chosen.serial_number != ias['serial_number'].native:\n            continue\n\n        if chosen.issuer == ias['issuer']:\n            return chosen\n\n    return None\n\n\ndef _cryptography_hash_function(algorithm: DigestAlgorithm) -> Union[None, Type[hashes.SHA1], Type[hashes.SHA256], Type[hashes.SHA512]]:\n    \"\"\"Find the cryptography hash function given the string output from asn1crypto SignedDigestAlgorithm.\n\n    Todo: There should be a better way to do this?\n\n    Args:\n          algorithm (DigestAlgorithm): The asn1crypto Signed Digest Algorithm\n    Returns:\n        Union[Type[hashes.SHA1], Type[hashes.SHA256], Type[hashes.SHA512]] A cryptography hash function for use with\n         signature verification.\n    \"\"\"\n\n    hash_algo = algorithm['algorithm'].native\n\n    if hash_algo == \"sha1\":\n        return hashes.SHA1\n    elif hash_algo == \"sha256\":\n        return hashes.SHA256\n    elif hash_algo == \"sha512\":\n        return hashes.SHA512\n    else:\n        return None\n\n\ndef _cryptography_pad_function(algorithm: SignedDigestAlgorithm) -> Union[None, Type[padding.PKCS1v15]]:\n    \"\"\"Find the cryptography pad function given a signed digest algorithm from asn1crypto.\n\n    Args:\n        algorithm (SignedDigestAlgorithm): The asn1crypto Signed Digest Algorithm\n    Returns:\n        Union[None, Type[padding.PKCS1v15]]: The padding function for the signed digest\n        \"\"\"\n    signature_algo = algorithm.signature_algo\n\n    if signature_algo == \"rsassa_pkcs1v15\":\n        return padding.PKCS1v15\n    else:\n        return None\n"
  },
  {
    "path": "commandment/cms/decorators.py",
    "content": "from typing import List, Tuple\n\nfrom asn1crypto.cms import CMSAttribute\nfrom cryptography.exceptions import InvalidSignature\nfrom flask import request, g, current_app, abort\nfrom functools import wraps\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom asn1crypto import cms\nfrom base64 import b64decode, b64encode\nfrom . import _certificate_by_signer_identifier, _cryptography_hash_function, _cryptography_pad_function\n\n\ndef _verify_cms_signers(signed_data: bytes, detached: bool = False) -> Tuple[List[x509.Certificate], bytes]:\n    ci = cms.ContentInfo.load(signed_data)\n    assert ci['content_type'].native == 'signed_data'\n    signed: cms.SignedData = ci['content']\n\n    current_app.logger.debug(\"CMS request contains %d certificate(s)\", len(signed['certificates']))\n\n    signers = []\n    for signer in signed['signer_infos']:\n        asn_certificate = _certificate_by_signer_identifier(signed['certificates'], signer['sid'])\n        assert asn_certificate is not None\n        certificate = x509.load_der_x509_certificate(asn_certificate.dump(), default_backend())\n\n        digest_algorithm = signer['digest_algorithm']\n        signature_algorithm = signer['signature_algorithm']\n\n        hash_function = _cryptography_hash_function(digest_algorithm)\n        pad_function = _cryptography_pad_function(signature_algorithm)\n\n        if hash_function is None or pad_function is None:\n            raise ValueError('Unsupported signature algorithm: {}'.format(signature_algorithm))\n        else:\n            current_app.logger.debug(\"Using signature algorithm: %s\", signature_algorithm.native)\n\n        assert signed['encap_content_info']['content_type'].native == 'data'\n\n        if detached:\n            data = request.data\n        else:\n            data = signed['encap_content_info']['content'].native\n\n        if 'signed_attrs' in signer and len(signer['signed_attrs']) > 0:\n            for i in range(0, len(signer['signed_attrs'])):\n                signed_attr: CMSAttribute = signer['signed_attrs'][i]\n\n                if signed_attr['type'].native == \"message_digest\":\n                    current_app.logger.debug(\"SignerInfo digest: %s\", b64encode(signed_attr['values'][0].native))\n\n            certificate.public_key().verify(\n                signer['signature'].native,\n                signer['signed_attrs'].dump(),\n                pad_function(),\n                hash_function()\n            )\n        else:  # No signed attributes means we are only validating the digest\n            certificate.public_key().verify(\n                signer['signature'].native,\n                data,\n                pad_function(),\n                hash_function()\n            )\n\n        signers.append(certificate)\n\n    # TODO: Don't assume that content is OctetString\n\n    if detached:\n        return signers, request.data\n    else:\n        return signers, signed['encap_content_info']['content'].native\n\n\ndef verify_cms_signers(f):\n    \"\"\"Verify the signers of a request containing a CMS/PKCS#7, DER encoded body.\n\n    The certificate of each signer is placed on the global **g** variable as **g.signers** and the signed data is\n    set as **g.signed_data**.\n\n    In unit tests, this decorator is completely disabled by the presence of testing = True\n\n    Raises:\n          - TypeError if *Content-Type* header is not \"application/pkcs7-signature\"\n          - SigningError if any signer on the CMS content is not valid.\n    \"\"\"\n    @wraps(f)\n    def decorator(*args, **kwargs):\n        if current_app.testing:\n            return f(*args, **kwargs)\n\n        current_app.logger.debug('Verifying CMS Request Data for request to %s', request.url)\n\n        if request.headers['Content-Type'] != \"application/pkcs7-signature\":\n            raise TypeError(\"verify_cms_signers expects application/pkcs7-signature, got: {}\".format(\n                request.headers['Content-Type']))\n\n        g.signers, g.signed_data = _verify_cms_signers(request.data)\n\n        return f(*args, **kwargs)\n\n    return decorator\n\n\ndef verify_mdm_signature(f):\n    \"\"\"Verify the signature supplied by the client in the request using the ``Mdm-Signature`` header.\n\n    If the authenticity of the message has been verified,\n    then the signer is attached to the **g** object as **g.signer**.\n\n    In unit tests, this decorator is completely disabled by the presence of app.testing = True.\n    You can also disable enforcement in dev by setting the flask setting DEBUG to true.\n\n    :reqheader Mdm-Signature: BASE64-encoded CMS Detached Signature of the message. (if `SignMessage` was true)\n    \"\"\"\n    @wraps(f)\n    def decorator(*args, **kwargs):\n        if current_app.testing:\n            return f(*args, **kwargs)\n\n        if 'Mdm-Signature' not in request.headers:\n            raise TypeError('Client did not supply an Mdm-Signature header but signature is required.')\n\n        detached_signature = b64decode(request.headers['Mdm-Signature'])\n\n        try:\n            signers, signed_data = _verify_cms_signers(detached_signature, detached=True)\n            g.signers = signers\n            g.signed_data = signed_data\n        except InvalidSignature as e:\n            current_app.logger.warn(\"Invalid Signature in Mdm-Signature header\")\n            if not current_app.config.get('DEBUG', False):\n                return abort(403)\n\n        return f(*args, **kwargs)\n\n    return decorator\n"
  },
  {
    "path": "commandment/dbtypes.py",
    "content": "from sqlalchemy.types import TypeDecorator, CHAR\nfrom sqlalchemy.dialects.postgresql import UUID\nimport uuid\nimport json\nfrom datetime import datetime\nfrom sqlalchemy.types import TypeDecorator\nfrom sqlalchemy import Text\n\n\nclass GUID(TypeDecorator):\n    \"\"\"Platform-independent GUID type.\n\n    Uses Postgresql's UUID type, otherwise uses\n    CHAR(32), storing as stringified hex values.\n\n    \"\"\"\n    impl = CHAR\n\n    def load_dialect_impl(self, dialect):\n        if dialect.name == 'postgresql':\n            return dialect.type_descriptor(UUID())\n        else:\n            return dialect.type_descriptor(CHAR(32))\n\n    def process_bind_param(self, value, dialect):\n        if value is None:\n            return value\n        elif dialect.name == 'postgresql':\n            return str(value)\n        else:\n            if not isinstance(value, uuid.UUID):\n                return \"%.32x\" % uuid.UUID(value).int\n            else:\n                # hexstring\n                return \"%.32x\" % value.int\n\n    def process_result_value(self, value, dialect):\n        if value is None:\n            return value\n        else:\n            return uuid.UUID(value)\n\n\ndef json_datetime_serializer(o):\n    \"\"\"Serialize datetime objects into ISO format string dates\n\n    Raises:\n        TypeError: If the https://mdmcert.download/api/v1/signrequestobject cannot be serialized.\n    \"\"\"\n\n    if isinstance(o, datetime):\n        return o.isoformat()\n\n    raise TypeError(repr(o) + \" is not JSON serializable\")\n\n\nclass JSONEncodedDict(TypeDecorator):\n    \"\"\"Represents an immutable structure as a json-encoded string\"\"\"\n    impl = Text\n\n    def process_bind_param(self, value, dialect):\n        if value is None:\n            return None\n\n        return json.dumps(value, separators=(',', ':'), default=json_datetime_serializer)\n\n    def process_result_value(self, value, dialect):\n        if not value:\n            return None\n\n        return json.loads(value)\n\n\nclass SetOfEnumValues(TypeDecorator):\n    \"\"\"Represents a Set of Enumeration values, encoded as a json array of enum names.\"\"\"\n    impl = Text\n\n    def __init__(self, *arg, **kw):\n        TypeDecorator.__init__(self, *arg, **kw)\n        self.values = arg[0]\n\n    def process_bind_param(self, value, dialect):  # type: (List[Enum], any) -> str\n        if value is None:\n            return None\n\n        return json.dumps([v.value for v in value], separators=(',', ':'), default=json_datetime_serializer)\n\n    def process_result_value(self, value, dialect):\n        if not value:\n            return None\n\n        values = json.loads(value)\n        evalues = [self.values(v) for v in values]\n        return evalues\n"
  },
  {
    "path": "commandment/decorators.py",
    "content": "from functools import wraps\n\nfrom flask import request, abort, current_app, g\nfrom cryptography import x509\nfrom cryptography.exceptions import UnsupportedAlgorithm\nfrom cryptography.hazmat.backends import default_backend\n\nimport plistlib\n\n\ndef parse_plist_input_data(f):\n    \"\"\"Parses plist data as HTTP input from request.\n\n    The unserialized data is attached to the global **g** object as **g.plist_data**.\n\n    :status 400: If invalid plist data was supplied in the request.\n    \"\"\"\n\n    @wraps(f)\n    def decorator(*args, **kwargs):\n        try:\n            if current_app.debug:\n                current_app.logger.debug(request.data)\n            g.plist_data = plistlib.loads(request.data)\n        except:\n            current_app.logger.info('could not parse property list input data')\n            abort(400, 'invalid input data')\n\n        return f(*args, **kwargs)\n\n    return decorator\n\n\ndef pem_certificate_upload(f):\n    \"\"\"Parse PEM formatted certificate in request data\n    \n    TODO: form field name option\n    \"\"\"\n\n    @wraps(f)\n    def decorator(*args, **kwargs):\n        try:\n            certificate_data = request.files['file'].read()\n            g.certificate = x509.load_pem_x509_certificate(certificate_data, backend=default_backend())\n        except UnsupportedAlgorithm as e:\n            current_app.logger.info('could not parse PEM certificate data')\n            abort(400, 'invalid input data')\n\n        return f(*args, **kwargs)\n\n    return decorator\n\n\n"
  },
  {
    "path": "commandment/default_settings.py",
    "content": "# Flask Dev Server\nPORT = 5443\n\n# Flask-Alembic imports configuration from here instead of the alembic.ini\nALEMBIC = {\n    'script_location': '%(here)s/alembic/versions'\n}\n\nALEMBIC_CONTEXT = {\n    'render_as_batch': True,  # Necessary to support SQLite ALTER on constraints\n}\n\n# Describes a static OAuth 2 Client which is the Commandment UI\nOAUTH2_CLIENT_UI = {\n    'client_id': 'F8955645-A21D-44AE-9387-42B0800ADF15',\n    'client_secret': 'A',\n    'token_endpoint_auth_method': 'client_secret_basic',\n    'grant_type': 'password',\n    'response_type': 'token',\n    'scope': 'profile',\n    'client_name': 'Commandment UI'\n}\n\n# http://flask-sqlalchemy.pocoo.org/2.1/config/\nSQLALCHEMY_DATABASE_URI = 'sqlite:///commandment/commandment.db'\n# FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.\nSQLALCHEMY_TRACK_MODIFICATIONS = False\n\n\n# PLEASE! Do not take this key and use it for another product/project. It's\n# only for Commandment's use. If you'd like to get your own (free!) key\n# contact the mdmcert.download administrators and get your own key for your\n# own project/product.  We're trying to keep statistics on which products are\n# requesting certs (per Apple T&C). Don't force Apple's hand and\n# ruin it for everyone!\nMDMCERT_API_KEY = 'b742461ff981756ca3f924f02db5a12e1f6639a9109db047ead1814aafc058dd'\n\nPLISTIFY_MIMETYPE = 'application/xml'\n\n\n# Internal CA - Certificate X.509 Attributes\nINTERNAL_CA_CN = 'COMMANDMENT-CA'\nINTERNAL_CA_O = 'Commandment'\n\n\n# --------------\n# SCEPy Defaults\n# --------------\n\n# Directory where certs, revocation lists, serials etc will be kept\nSCEPY_CA_ROOT = \"CA\"\n\n# X.509 Name Attributes used to generate the CA Certificate\nSCEPY_CA_X509_CN = 'SCEPY-CA'\nSCEPY_CA_X509_O = 'SCEPy'\nSCEPY_CA_X509_C = 'US'\n\n# Force a single certificate to be returned as a PKCS#7 Degenerate instead of raw DER data\nSCEPY_FORCE_DEGENERATE_FOR_SINGLE_CERT = False\n\n\n# These applications will not be shown in inventory.\nIGNORED_APPLICATION_BUNDLE_IDS = [\n    'com.apple.MigrateAssistant',\n    'com.apple.keychainaccess',\n    'com.apple.grapher',\n    'com.apple.Grab',\n    'com.apple.ActivityMonitor',\n    'com.apple.backup.launcher',  # Time Machine\n    'com.apple.TextEdit',\n    'com.apple.systempreferences',\n    'com.apple.CoreLocationAgent',\n    'com.apple.CaptiveNetworkAssistant',\n    'com.apple.CalendarFileHandler',\n    'com.apple.BluetoothUIServer',\n    'com.apple.BluetoothSetupAssistant',\n    'com.apple.AutomatorRunner',\n    'com.apple.AppleFileServer',\n    'com.apple.AirportBaseStationAgent',\n    'com.apple.AirPlayUIAgent',\n    'com.apple.AddressBook.UrlForwarder',\n    'com.apple.AVB-Audio-Configuration',\n    'com.apple.ScriptMonitor',\n    'com.apple.ScreenSaver.Engine',\n    'com.apple.systemevents',\n    'com.apple.stocks',\n    'com.apple.Spotlight',\n    'com.apple.SoftwareUpdate',\n    'com.apple.SocialPushAgent',\n    'com.apple.Siri',\n    'com.apple.screencapturetb',\n    'com.apple.rcd',\n    'com.apple.CloudKit.ShareBear',\n    'com.apple.cloudphotosd',\n    'com.apple.wifi.WiFiAgent',\n    'com.apple.weather',\n    'com.apple.VoiceOver',\n    'com.apple.UserNotificationCenter',\n    'com.apple.UnmountAssistantAgent',\n    'com.apple.UniversalAccessControl',\n    'com.apple.Ticket-Viewer',\n    'com.apple.ThermalTrap',\n    'com.apple.systemuiserver',\n    'com.apple.check_afp',\n    'com.apple.AddressBook.sync',\n    'com.apple.AddressBookSourceSync',\n    'com.apple.AddressBook.abd',\n    'com.apple.ABAssistantService',\n    'com.apple.FontRegistryUIAgent',\n    'com.apple.speech.synthesis.SpeechSynthesisServer',\n    'com.apple.print.PrinterProxy',\n    'com.apple.StorageManagementLauncher',\n    'com.apple.Terminal',\n    'com.apple.PhotoBooth',\n    'com.apple.mail',\n    'com.apple.notificationcenter.widgetsimulator',\n    'com.apple.quicklook.ui.helper',\n    'com.apple.quicklook.QuickLookSimulator',\n    'com.apple.QuickLookDaemon32',\n    'com.apple.QuickLookDaemon',\n    'com.apple.syncserver',\n    'com.apple.WebKit.PluginHost',\n    'com.apple.AirScanScanner',\n    'com.apple.MakePDF',\n    'com.apple.BuildWebPage',\n    'com.apple.VIM-Container',\n    'com.apple.TrackpadIM-Container',\n    'com.apple.inputmethod.Tamil',\n    'com.apple.TCIM-Container',\n    'com.apple.exposelauncher',\n    'com.apple.iChat',\n    'com.apple.Maps',\n    'com.apple.launchpad.launcher',\n    'com.apple.FaceTime',\n    'com.apple.Dictionary',\n    'com.apple.dashboardlauncher',\n    'com.apple.DVDPlayer',\n    'com.apple.Chess',\n    'com.apple.iCal',\n    'com.apple.calculator',\n    'com.apple.Automator',\n    'com.apple.KIM-Container',\n    'com.apple.CharacterPaletteIM',\n    'com.apple.inputmethod.AssistiveControl',\n    'com.apple.VirtualScanner',\n    'com.apple.Type8Camera',\n    'com.apple.loginwindow',\n    'com.apple.SetupAssistant',\n    'com.apple.PhotoLibraryMigrationUtility',\n    'com.apple.notificationcenterui',\n    'com.apple.ManagedClient',\n    'com.apple.helpviewer',\n    'com.apple.finder.Open-iCloudDrive',\n    'com.apple.finder.Open-Recents',\n    'com.apple.finder.Open-Network',\n    'com.apple.finder.Open-Computer',\n    'com.apple.finder.Open-AllMyFiles',\n    'com.apple.finder.Open-AirDrop',\n    'com.apple.finder',\n    'com.apple.dock',\n    'com.apple.coreservices.uiagent',\n    'com.apple.controlstrip',\n    'com.apple.CertificateAssistant',\n    'com.apple.wifi.diagnostics',\n    'com.apple.SystemImageUtility',\n    'com.apple.RAIDUtility',\n    'com.apple.NetworkUtility',\n    'com.apple.FolderActionsSetup',\n    'com.apple.DirectoryUtility',\n    'com.apple.AboutThisMacLauncher',\n    'com.apple.AppleScriptUtility',\n    'com.apple.AppleGraphicsWarning',\n    'com.apple.print.add',\n    'com.apple.archiveutility',\n    'com.apple.appstore',\n    'com.apple.Console',\n    'com.apple.bootcampassistant',\n    'com.apple.BluetoothFileExchange',\n    'com.apple.siri.launcher',\n    'com.apple.reminders',\n    'com.apple.QuickTimePlayerX',\n    'com.apple.Image_Capture',\n    'com.apple.accessibility.universalAccessAuthWarn',\n    'com.apple.accessibility.universalAccessHUD',\n    'com.apple.accessibility.DFRHUD',\n    'com.apple.syncservices.syncuid',\n    'com.apple.syncservices.ConflictResolver',\n    'com.apple.STMFramework.UIHelper',\n    'com.apple.speech.SpeechRecognitionServer',\n    'com.apple.speech.SpeechDataInstallerd',\n    'com.apple.ScreenReaderUIServer',\n    'com.apple.PubSubAgent',\n    'com.apple.nbagent',\n    'com.apple.soagent',\n    'com.apple.imtransferservices.IMTransferAgent',\n    'com.apple.IMAutomaticHistoryDeletionAgent',\n    'com.apple.imagent',\n    'com.apple.imavagent',\n    'com.apple.idsfoundation.IDSRemoteURLConnectionAgent',\n    'com.apple.identityservicesd',\n    'com.apple.FindMyMacMessenger',\n    'com.apple.Family',\n    'com.apple.familycontrols.useragent',\n    'com.apple.eap8021x.eaptlstrust',\n    'com.apple.frameworks.diskimages.diuiagent',\n    'com.apple.FollowUpUI',\n    'com.apple.CCE.CIMFindInputCode',\n    'com.apple.cmfsyncagent',\n    'com.apple.storeuid',\n    'com.apple.lateragent',\n    'com.apple.bird',  # iCloud Drive\n    'com.apple.AskPermissionUI',\n    'com.apple.Calibration-Assistant',\n    'com.apple.AccessibilityVisualsAgent',\n    'com.apple.AOSPushRelay',\n    'com.apple.AOSHeartbeat',\n    'com.apple.AOSAlertManager',\n    'com.apple.iCloudUserNotificationsd',\n    'com.apple.SCIM-Container',\n    'com.apple.PAH-Container',\n    'com.apple.inputmethod.PluginIM',\n    'com.apple.KeyboardViewer',\n    'com.apple.PIPAgent',\n    'com.apple.OSDUIHelper',\n    'com.apple.ODSAgent',\n    'com.apple.OBEXAgent',\n    'com.apple..NowPlayingWidgetContainer',\n    'com.apple.NowPlayingTouchUI',\n    'com.apple.NetAuthAgent',\n    'com.apple.MemorySlotUtility',\n    'com.apple.locationmenu',\n    'com.apple.Language-Chooser',\n    'com.apple.security.Keychain-Circle-Notification',\n    'com.apple.KeyboardSetupAssistant',\n    'com.apple.JavaWebStart',\n    'com.apple.JarLauncher',\n    'com.apple.Installer-Progress',\n    'com.apple.PackageKit.Install-in-Progress',\n    'com.apple.dt.CommandLineTools.installondemand',\n    'com.apple.imageevents',\n    'com.apple.gamecenter',\n    'com.apple.FolderActionsDispatcher',\n    'com.apple.ExpansionSlotUtility',\n    'com.apple.EscrowSecurityAlert',\n    'com.apple.DwellControl',\n    'com.apple.DiscHelper',\n    'com.apple.databaseevents',\n    'com.apple.ColorSyncCalibrator',\n    'com.apple.print.AirScanLegacyDiscovery',\n    'com.apple.ScriptEditor.id.image-file-processing-droplet-template',\n    'com.apple.ScriptEditor.id.file-processing-droplet-template',\n    'com.apple.ScriptEditor.id.droplet-with-settable-properties-template',\n    'com.apple.ScriptEditor.id.cocoa-applet-template',\n    'com.apple.inputmethod.Ainu',\n    'com.apple.50onPaletteIM',\n    'com.apple.AutoImporter',\n    'com.apple.Type5Camera',\n    'com.apple.Type4Camera',\n    'com.apple.PTPCamera',\n    'com.apple.MassStorageCamera',\n    'com.apple.imautomatichistorydeletionagent',\n    'com.apple.SyncServices.AppleMobileSync',\n    'com.apple.SyncServices.AppleMobileDeviceHelper',\n    'com.apple.coreservices.UASharedPasteboardProgressUI',\n    'com.apple.SummaryService',\n    'com.apple.ImageCaptureService',\n    'com.apple.ChineseTextConverterService',\n    'com.apple.Pass-Viewer',\n    'com.apple.PowerChime',\n    'com.apple.ProblemReporter',\n    'com.apple.pluginIM.pluginIMRegistrator',\n    'com.apple.ReportPanic',\n    'com.apple.RemoteDesktopAgent',\n    'com.apple.RapportUIAgent',\n    'com.apple.MRT',\n    'com.apple.AirPortBaseStationAgent',\n    'com.apple.appstore.AppDownloadLauncher',\n    'com.apple.appleseed.FeedbackAssistant',\n    'com.apple.ScreenSharing',\n    'com.apple.FirmwareUpdateHelper',\n    'com.apple.SecurityFixer',\n    'com.apple.ZoomWindow.app',\n    'com.apple.IMServicePlugInAgent',\n    'com.apple.itunes.connect.ApplicationLoader',\n    'com.apple.DiskImageMounter',\n    'com.apple.NetworkDiagnostics',\n    'com.apple.installer',\n    'com.apple.VoiceOverQuickstart',\n]\n"
  },
  {
    "path": "commandment/dep/__init__.py",
    "content": "from typing import Set, Dict\nfrom enum import Enum\n\n\nclass SetupAssistantStep(Enum):\n    \"\"\"This enumeration contains all possible steps of Setup Assistant that can be skipped.\n\n    See Also:\n          - `DEP Web Services: Define Profile <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/4-Profile_Management/ProfileManagement.html#//apple_ref/doc/uid/TP40017387-CH7-SW30>`_.\n    \"\"\"\n    \"\"\"Skips Apple ID setup.\"\"\"\n    AppleID = 'AppleID'\n    \"\"\"Skips Touch ID setup.\"\"\"\n    Biometric = 'Biometric'\n    \"\"\"Disables automatically sending diagnostic information.\"\"\"\n    Diagnostics = 'Diagnostics'\n    \"\"\"Skips DisplayTone setup.\"\"\"\n    DisplayTone = 'DisplayTone'\n    \"\"\"Disables Location Services.\"\"\"\n    Location = 'Location'\n    \"\"\"Hides and disables the passcode pane.\"\"\"\n    Passcode = 'Passcode'\n    \"\"\"Skips Apple Pay setup.\"\"\"\n    Payment = 'Payment'\n    \"\"\"Skips privacy pane.\"\"\"\n    Privacy = 'Privacy'\n    \"\"\"Disables restoring from backup.\"\"\"\n    Restore = 'Restore'\n    SIMSetup = 'SIMSetup'\n    \"\"\"Disables Siri.\"\"\"\n    Siri = 'Siri'\n    \"\"\"Skips Terms and Conditions.\"\"\"\n    TOS = 'TOS'\n    \"\"\"Skips zoom setup.\"\"\"\n    Zoom = 'Zoom'\n    \"\"\"If the Restore pane is not skipped, removes Move from Android option from it.\"\"\"\n    Android = 'Android'\n    \"\"\"Skips the Home Button screen in iOS.\"\"\"\n    HomeButtonSensitivity = 'HomeButtonSensitivity'\n    \"\"\"Skips on-boarding informational screens for user education (“Cover Sheet, Multitasking & Control Center”, \n        for example) in iOS.\"\"\"\n    iMessageAndFaceTime = 'iMessageAndFaceTime'\n    \"\"\"Skips the iMessage and FaceTime screen in iOS.\"\"\"\n    OnBoarding = 'OnBoarding'\n    \"\"\"Skips the screen for Screen Time in iOS.\"\"\"\n    ScreenTime = 'ScreenTime'\n    \"\"\"Skips the mandatory software update screen in iOS.\"\"\"\n    SoftwareUpdate = 'SoftwareUpdate'\n    \"\"\"Skips the screen for watch migration in iOS.\"\"\"\n    WatchMigration = 'WatchMigration'\n    \"\"\"Skips the Choose Your Look screen in macOS.\"\"\"\n    Appearance = 'Appearance'\n    \"\"\"Disables FileVault Setup Assistant screen in macOS.\"\"\"\n    FileVault = 'FileVault'\n    \"\"\"Skips iCloud Analytics screen in macOS.\"\"\"\n    iCloudDiagnostics = 'iCloudDiagnostics'\n    \"\"\"Skips iCloud Documents and Desktop screen in macOS.\"\"\"\n    iCloudStorage = 'iCloudStorage'\n    \"\"\"Disables registration screen in macOS\"\"\"\n    Registration = 'Registration'\n      \n    #  ATV\n    \"\"\"Skips the tvOS screen about using aerial screensavers in ATV.\"\"\"\n    ScreenSaver = 'ScreenSaver'\n    \"\"\"Skips the Tap To Set Up option in ATV about using an iOS device to set up your ATV (instead of entering all \n        your account information and setting choices separately).\"\"\"\n    TapToSetup = 'TapToSetup'\n    \"\"\"Skips TV home screen layout sync screen in tvOS.\"\"\"\n    TVHomeScreenSync = 'TVHomeScreenSync'\n    \"\"\"Skips the TV provider sign in screen in tvOS.\"\"\"\n    TVProviderSignIn = 'TVProviderSignIn'\n    \"\"\"Skips the “Where is this Apple TV?” screen in tvOS.\"\"\"\n    TVRoom = 'TVRoom'\n\n\nSkipSetupSteps = Set[SetupAssistantStep]\n\n\nclass DEPProfileRemovalStatus(Enum):\n    SUCCESS = \"SUCCESS\"\n    NOT_ACCESSIBLE = \"NOT_ACCESSIBLE\"\n    FAILED = \"FAILED\"\n\n\nSerialNumber = str\nDEPProfileRemovals = Dict[SerialNumber, DEPProfileRemovalStatus]\n\n\nclass DEPOrgType(Enum):\n    \"\"\"This enum specifies allowable values for the ``org_type`` field of the dep /account endpoint.\"\"\"\n    Education = 'edu'\n    Organization = 'org'\n\n\nclass DEPOrgVersion(Enum):\n    \"\"\"This enum specifies allowable values for the ``org_version`` field of the dep /account endpoint.\"\"\"\n    v1 = 'v1'  # Apple Deployment Programmes\n    v2 = 'v2'  # Apple School Manager\n\n\nclass DEPOperationType(Enum):\n    \"\"\"This enum describes the types of operations returned in a DEP Sync Devices result.\"\"\"\n    Added = 'added'\n    Modified = 'modified'\n    Deleted = 'deleted'\n"
  },
  {
    "path": "commandment/dep/app.py",
    "content": "import sqlalchemy.orm.exc\nimport datetime\nimport dateutil.parser\n\nfrom flask import Blueprint, jsonify, g, current_app, abort, request\nfrom flask_rest_jsonapi import Api\nfrom cryptography.hazmat.primitives.serialization import Encoding\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography import x509\nfrom cryptography.x509 import NameOID\nfrom base64 import urlsafe_b64encode\n\nfrom commandment.models import db\nfrom commandment.pki.models import RSAPrivateKey, CertificateSigningRequest\nfrom commandment.dep.models import DEPServerTokenCertificate, DEPAccount\nfrom commandment.enroll.util import generate_enroll_profile\nfrom commandment.cms.decorators import verify_cms_signers\nfrom commandment.plistutil.nonewriter import dumps as dumps_none\nfrom commandment.profiles.plist_schema import ProfileSchema\nfrom commandment.profiles import PROFILE_CONTENT_TYPE\nfrom commandment.pki.ca import get_ca\nfrom commandment.dep import smime\n\nfrom .resources import DEPProfileList, DEPProfileDetail, DEPProfileRelationship, DEPAccountList, DEPAccountDetail\nimport plistlib\nimport json\n\ndep_app = Blueprint('dep_app', __name__)\napi = Api(blueprint=dep_app)\n\napi.route(DEPProfileList, 'dep_profiles_list', '/api/v1/dep/profiles/',\n          '/api/v1/dep/accounts/<int:dep_account_id>/profiles')\napi.route(DEPProfileDetail, 'dep_profile_detail', '/api/v1/dep/profiles/<int:dep_profile_id>')\napi.route(DEPProfileRelationship, 'dep_profile_devices',\n          '/api/v1/dep/profiles/<int:dep_profile_id>/relationships/devices')\napi.route(DEPProfileRelationship, 'dep_profile_dep_account',\n          '/api/v1/dep/profiles/<int:dep_profile_id>/relationships/dep_account')\napi.route(DEPAccountList, 'dep_accounts_list', '/api/v1/dep/accounts/')\napi.route(DEPAccountDetail, 'dep_account_detail', '/api/v1/dep/accounts/<int:dep_account_id>')\n\n\n@dep_app.route('/dep/certificate/download', methods=[\"GET\"])\ndef certificate_download():\n    \"\"\"Create a new key/certificate to upload to the DEP/ASM/ABM portal.\n\n    The private key generated for this certificate will be the key recipient of the DEP S/MIME payload.\n    \"\"\"\n\n    try:\n        certificate_model = db.session.query(DEPServerTokenCertificate).filter_by(x509_cn='COMMANDMENT-DEP').one()\n    except sqlalchemy.orm.exc.NoResultFound:\n        ca = get_ca()\n        private_key = rsa.generate_private_key(\n            public_exponent=65537,\n            key_size=2048,\n            backend=default_backend(),\n        )\n        private_key_model = RSAPrivateKey.from_crypto(private_key)\n        db.session.add(private_key_model)\n\n        name = x509.Name([\n            x509.NameAttribute(NameOID.COMMON_NAME, 'COMMANDMENT-DEP'),\n            x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'commandment')\n        ])\n\n        builder = x509.CertificateSigningRequestBuilder()\n        builder = builder.subject_name(name)\n        builder = builder.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)\n\n        request = builder.sign(\n            private_key,\n            hashes.SHA256(),\n            default_backend()\n        )\n        request_model = CertificateSigningRequest.from_crypto(request)\n        request_model.rsa_private_key = private_key_model\n        db.session.add(request_model)\n\n        certificate = ca.sign(request)\n        certificate_model = DEPServerTokenCertificate.from_crypto(certificate)\n        certificate_model.rsa_private_key = private_key_model\n        db.session.add(certificate_model)\n\n        db.session.commit()\n\n    return certificate_model.pem_data, 200, {'Content-Type': 'application/x-x509-ca-cert',\n                                             'Content-Disposition': 'attachment; filename=\"commandment-dep.cer\"'}\n\n\n@dep_app.route('/dep/stoken/upload', methods=[\"POST\"])\ndef stoken_upload():\n    \"\"\"Upload the smime.p7m supplied from the DEP, ASM or ABM portals and decrypt it with a matching private key from\n    our database, storing the result in the ``dep_configurations`` table.\n\n    :reqheader Accept: application/vnd.api+json\n    :reqheader Content-Type: multipart/form-data\n    :statuscode 200: token decrypted ok\n    :statuscode 400: token was unable to be decrypted.\n    :statuscode 500: system error\n    \"\"\"\n    if 'file' not in request.files:\n        abort(400, 'no file uploaded in request data')\n\n    f = request.files['file']\n\n    try:\n        certificate_model = db.session.query(DEPServerTokenCertificate).filter_by(x509_cn='COMMANDMENT-DEP').one()\n    except sqlalchemy.orm.exc.NoResultFound:\n        return abort(400, \"No DEP certificate generated, impossible to decrypt the DEP token\")\n\n    pk: RSAPrivateKey = certificate_model.rsa_private_key\n    if pk is None:\n        return abort(500, 'Missing RSA Private Key for uploaded DEP token.')\n    pk_crypto = pk.to_crypto()\n\n    smime_data = f.read()\n    payload = smime.decrypt(smime_data, pk_crypto)\n\n    # dirty, dirty hacks for now. python email does not strip boundaries\n    payload = payload.replace('-----BEGIN MESSAGE-----', '').replace('-----END MESSAGE-----', '')\n\n    try:\n        stoken = json.loads(payload)\n    except json.decoder.JSONDecodeError:\n        current_app.logger.debug(payload)\n        return abort(400, \"Failed to decode token, could not parse JSON data inside S/MIME data\")\n\n    try:\n        dep_account = db.session.query(DEPAccount).one()\n    except sqlalchemy.orm.exc.NoResultFound:\n        dep_account = DEPAccount()\n\n    dep_account.certificate = certificate_model\n    dep_account.consumer_key = stoken['consumer_key']\n    dep_account.consumer_secret = stoken['consumer_secret']\n    dep_account.access_token = stoken['access_token']\n    dep_account.access_secret = stoken['access_secret']\n    dep_account.access_token_expiry = dateutil.parser.parse(stoken['access_token_expiry'])\n    dep_account.token_updated_at = datetime.datetime.utcnow()\n\n    db.session.commit()\n    current_app.logger.debug('Saved DEP stoken')\n\n    return jsonify(stoken)\n\n\n@dep_app.route('/dep/enroll', methods=[\"POST\"])\n@verify_cms_signers\ndef profile():\n    \"\"\"Accept a CMS Signed DER encoded XML data containing device information.\n\n    This starts the DEP enrollment process. The absolute url to this endpoint should be present in the DEP profile's\n    enrollment URL.\n\n    The signed data contains a plist with the following keys:\n\n    :UDID: The device’s UDID.\n    :SERIAL: The device's Serial Number.\n    :PRODUCT: The device’s product type: e.g., iPhone5,1.\n    :VERSION: The OS version installed on the device: e.g., 7A182.\n    :IMEI: The device’s IMEI (if available).\n    :MEID: The device’s MEID (if available).\n    :LANGUAGE: The user’s currently-selected language: e.g., en.\n\n    See Also:\n        - `Mobile Device Management Protocol: Request to a Profile URL\n            <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/4-Profile_Management/ProfileManagement.html#//apple_ref/doc/uid/TP40017387-CH7-SW242>`_.\n    \"\"\"\n    g.plist_data = plistlib.loads(g.signed_data)\n    profile = generate_enroll_profile()\n\n    schema = ProfileSchema()\n    result = schema.dump(profile)\n    plist_data = dumps_none(result.data, skipkeys=True)\n\n    return plist_data, 200, {'Content-Type': PROFILE_CONTENT_TYPE}\n\n\n@dep_app.route('/dep/anchor_certs', methods=[\"GET\"])\ndef anchor_certs():\n    \"\"\"Download a list of certificates to trust the MDM\n\n    The response is a JSON array of base64 encoded DER certs as described in the DEP profile creation documentation.\"\"\"\n    anchors = []\n\n    if 'CA_CERTIFICATE' in current_app.config:\n        with open(current_app.config['CA_CERTIFICATE'], 'rb') as fd:\n            pem_data = fd.read()\n            c: x509.Certificate = x509.load_pem_x509_certificate(pem_data, backend=default_backend())\n            der = c.public_bytes(Encoding.DER)\n            anchors.append(urlsafe_b64encode(der))\n\n    if 'SSL_CERTIFICATE' in current_app.config:\n        with open(current_app.config['SSL_CERTIFICATE'], 'rb') as fd:\n            pem_data = fd.read()\n            c: x509.Certificate = x509.load_pem_x509_certificate(pem_data, backend=default_backend())\n            der = c.public_bytes(Encoding.DER)\n            anchors.append(urlsafe_b64encode(der))\n\n    return jsonify(anchors)\n"
  },
  {
    "path": "commandment/dep/apple_schema.py",
    "content": "from marshmallow import fields, Schema\nfrom marshmallow_enum import EnumField\nfrom . import SetupAssistantStep\n\n\nclass AppleDEPProfileSchema(Schema):\n    \"\"\"marshmallow schema for a DEP profile.\n\n    See Also:\n        - `/profile endpoint <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/4-Profile_Management/ProfileManagement.html#//apple_ref/doc/uid/TP40017387-CH7-SW6>`_.\n        - `Mobile Device Management Protocol Reference <https://developer.apple.com/enterprise/documentation/MDM-Protocol-Reference.pdf>`_ \"Define Profile\" pg. 120\n    \"\"\"\n    # uuid = fields.UUID(dump_only=True)\n    # \"\"\"str: The Apple assigned UUID of this DEP Profile\"\"\"\n\n    profile_name = fields.String(required=True)\n    \"\"\"str: A human-readable name for the profile.\"\"\"\n    url = fields.Url(required=False)  # Should be required\n    \"\"\"str: The URL of the MDM server.\"\"\"\n    allow_pairing = fields.Boolean(default=True)\n    \"\"\"bool: If true, any device can pair with this device, supervision certs are not required.\"\"\"\n    is_supervised = fields.Boolean(default=False)\n    \"\"\"bool: If true, the device must be supervised\"\"\"\n    is_multi_user = fields.Boolean(default=False)\n    \"\"\"bool: If true, tells the device to configure for Shared iPad.\"\"\"\n    is_mandatory = fields.Boolean(default=False)\n    \"\"\"bool: If true, the user may not skip applying the profile returned by the MDM server\"\"\"\n    await_device_configured = fields.Boolean()\n    \"\"\"bool: If true, Setup Assistant does not continue until the MDM server sends DeviceConfigured.\"\"\"\n    is_mdm_removable = fields.Boolean()\n    \"\"\"bool: If false, the MDM payload delivered by the configuration URL cannot be removed by the user via the user \n    interface on the device\"\"\"\n    support_phone_number = fields.String(allow_none=True)\n    \"\"\"str: A support phone number for the organization.\"\"\"\n    auto_advance_setup = fields.Boolean()\n    \"\"\"bool: If set to true, the device will tell tvOS Setup Assistant to automatically advance though its screens.\"\"\"\n    support_email_address = fields.String(allow_none=True)  # No need to perform validation here\n    \"\"\"str: A support email address for the organization.\"\"\"\n    org_magic = fields.String(allow_none=True)\n    \"\"\"str: A string that uniquely identifies various services that are managed by a single organization.\"\"\"\n    anchor_certs = fields.List(fields.String())\n    \"\"\"List[str]: Each string should contain a DER-encoded certificate converted to Base64 encoding. If provided, \n    these certificates are used as trusted anchor certificates when evaluating the trust of the connection \n    to the MDM server URL.\"\"\"\n    supervising_host_certs = fields.List(fields.String())\n    \"\"\"List[str]: Each string contains a DER-encoded certificate converted to Base64 encoding. If provided, \n    the device will continue to pair with a host possessing one of these certificates even when allow_pairing \n    is set to false\"\"\"\n    skip_setup_items = fields.List(EnumField(SetupAssistantStep))\n    \"\"\"Set[SetupAssistantStep]: A list of setup panes to skip\"\"\"\n    department = fields.String(allow_none=True)\n    \"\"\"str: The user-defined department or location name.\"\"\"\n"
  },
  {
    "path": "commandment/dep/cli.py",
    "content": "import argparse\nimport logging\nimport asyncio\nfrom commandment.dep.dep import DEP\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"consumer_key\", help=\"The decrypted consumer_key from the DEP stoken\")\nparser.add_argument(\"consumer_secret\", help=\"The decrypted consumer_secret from the DEP stoken\")\nparser.add_argument(\"access_token\", help=\"The decrypted access_token from the DEP stoken\")\nparser.add_argument(\"access_secret\", help=\"The decrypted access_secret from the DEP stoken\")\nparser.add_argument(\"--url\", help=\"The URL of the DEP Service\", default=\"https://mdmenrollment.apple.com\")\n\nlogger = logging.getLogger(__name__)\nlogging.getLogger('asyncio').setLevel(logging.WARNING)\n\nasync def initial_dep_fetch(dep: DEP):\n    \"\"\"Perform the initial DEP fetch, if required.\"\"\"\n    for page in dep.devices():\n        for device in page:\n            pass\n\nasync def dep_sync(consumer_key: str, consumer_secret: str, access_token: str, access_secret: str, url: str):\n    dep = DEP(consumer_key, consumer_secret, access_token, access_secret, url)\n    initial_fetch = await initial_dep_fetch(dep)\n\n\ndef main():\n    args = parser.parse_args()\n    logging.basicConfig(level=logging.DEBUG)\n    loop = asyncio.get_event_loop()\n\n    loop.run_until_complete(dep_sync(\n        args.consumer_key,\n        args.consumer_secret,\n        args.access_token,\n        args.access_secret,\n        args.url,\n    ))\n\n    try:\n        loop.run_forever()\n    finally:\n        loop.run_until_complete(loop.shutdown_asyncgens())\n        loop.close()\n    \n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "commandment/dep/dep.py",
    "content": "from collections.abc import Iterator\nfrom typing import Union, List, Optional\nimport requests\nfrom requests.auth import AuthBase\nfrom requests_oauthlib import OAuth1\nimport re\nfrom datetime import timedelta, datetime\nfrom dateutil import parser as dateparser\nfrom locale import atof\nimport json\nimport logging\nfrom flask import g, current_app\n\nfrom commandment.dep import DEPProfileRemovals\nfrom .errors import DEPServiceError, DEPClientError\nfrom email.utils import parsedate  # Necessary for HTTP-Date\n\nlogger = logging.getLogger(__name__)\n\n\n# def get_dep():  # type: () -> DEP\n#     dep = getattr(g, '_dep', None)\n#\n#     if dep is None:\n#         dep_account: DEPAccount = db.session.query(DEPAccount).one()\n#         dep = DEP(\n#             consumer_key=dep_account.consumer_key,\n#             consumer_secret=dep_account.consumer_secret,\n#             access_token=dep_account.access_token,\n#             access_secret=dep_account.access_secret,\n#         )\n#\n#         g._dep = dep\n#\n#     return dep\n\n\nclass DEPAuth(AuthBase):\n    \"\"\"Attach X-ADM-Auth-Session token to the request.\n\n    Example:\n          session.get(\"https://something\", auth=DEPAuth(token))\n    \"\"\"\n    def __init__(self, token: str) -> None:\n        self.token = token\n\n    def __call__(self, r):\n        r.headers['X-ADM-Auth-Session'] = self.token\n        return r\n\n\nclass DEP:\n\n    UserAgent = 'commandment'\n\n    def __init__(self,\n                 consumer_key: str = None,\n                 consumer_secret: str = None,\n                 access_token: str = None,\n                 access_secret: str = None,\n                 access_token_expiry: Optional[str] = None,\n                 url: str = \"https://mdmenrollment.apple.com\") -> None:\n\n        self._session_token: Optional[str] = None\n        self._oauth = OAuth1(\n            consumer_key,\n            client_secret=consumer_secret,\n            resource_owner_key=access_token,\n            resource_owner_secret=access_secret,\n        )\n\n        if access_token_expiry is not None:\n            access_token_expiry_date = dateparser.parse(access_token_expiry)\n            self._access_token_expiry = access_token_expiry_date\n        else:\n            self._access_token_expiry = None\n\n        self._url = url\n        self._session = requests.session()\n        self._session.headers.update({\n            \"X-Server-Protocol-Version\": \"3\",\n            \"Content-Type\": \"application/json;charset=UTF8\",\n            \"User-Agent\": DEP.UserAgent,\n        })\n        self._retry_after: Optional[datetime] = None\n\n    @property\n    def session_token(self) -> Optional[str]:\n        return self._session_token\n\n    @classmethod\n    def from_token(cls, token: str):  # (str) -> DEP\n        \"\"\"Instantiate the DEP client instance from a string holding the service token json content.\"\"\"\n        stoken = json.loads(token)\n        return cls(**stoken)\n\n    def _response_hook(self, r: requests.Response, *args, **kwargs):\n        \"\"\"This method always exists as a response hook in order to keep some of the state returned by the\n        DEP service internally such as:\n            - The last value of the `X-ADM-Auth-Session` header, which is used on subsequent requests.\n            - The last value of the `Retry-After` header, which is used to set an instance variable to indicate\n                when we may make another request.\n\n        See Also:\n            - `Footnote about **X-ADM-Auth-Session** under Response Payload <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/4-Profile_Management/ProfileManagement.html#//apple_ref/doc/uid/TP40017387-CH7-SW2>`_.\n        \"\"\"\n        if r.status_code == 401:  # Token may be expired, or token is invalid\n            pass  # TODO: Need token refresh as decorator\n\n        # If the service gives us another session token, that replaces our current token.\n        if 'X-ADM-Auth-Session' in r.headers:\n            self._session_token = r.headers['X-ADM-Auth-Session']\n\n        # If the service wants to rate limit us, store that information locally.\n        if 'Retry-After' in r.headers:\n            after = r.headers['Retry-After']\n            if re.compile(r\"/[0-9]+/\").match(after):\n                d = timedelta(seconds=atof(after))\n                self._retry_after = datetime.utcnow() + d\n            else:  # HTTP Date\n                self._retry_after = datetime(*parsedate(after)[:6])\n\n    def send(self, req: requests.Request, **kwargs) -> Optional[requests.Response]:\n        \"\"\"Send a request to the DEP service.\n\n        If the service responds that the token has expired, fetch a new session token and re-issue the request.\n\n        Args:\n              req (requests.Request): The request, which will have DEP auth headers added to it.\n        Returns:\n              requests.Response: The response\n        \"\"\"\n        if self._access_token_expiry is not None and datetime.now() > self._access_token_expiry:\n            raise DEPClientError(\"DEP Service Token has expired, please generate a new one.\")\n\n        if self._retry_after is not None:  # refuse to send request\n            return None\n\n        if self.session_token is None:\n            self.fetch_token()\n\n        req.hooks = dict(response=self._response_hook)\n        req.auth = DEPAuth(self._session_token)\n\n        prepared = self._session.prepare_request(req)\n\n        res = self._session.send(prepared, **kwargs)\n\n        try:\n            res.raise_for_status()\n        except requests.HTTPError as e:\n            raise DEPServiceError(response=res, request=res.request) from e\n\n        return res\n\n    def fetch_token(self) -> Union[str, None]:\n        \"\"\"Request a new session token using our DEP credentials.\n\n        Returns:\n              Union[str, None]: The token that was returned (already set on this instance), or None if it failed.\n        \"\"\"\n        res = self._session.get(self._url + \"/session\", auth=self._oauth)\n        try:\n            res.raise_for_status()\n        except requests.HTTPError as e:\n            raise DEPServiceError(response=res, request=res.request) from e\n\n        self._session_token = res.json().get(\"auth_session_token\", None)\n        return self._session_token\n\n    def account(self) -> Union[None, dict]:\n        \"\"\"Get Account Details\n\n        The details are returned in the following dict format::\n\n            {\n                'server_name': 'MDM Server Name entered in the portal',\n                'server_uuid': '<32 char UUID without separators>',\n                'facilitator_id': 'E-mail of facilitator',\n                'admin_id': 'Administrator E-mail Address',\n                'org_name': 'Organization Name',\n                'org_email': 'Organization E-mail',\n                'org_phone': 'Organization Contact Phone',\n                'org_address': 'Organization Physical Address'\n            }\n\n        Returns:\n               Union[None, dict]: The account information, or None if it failed.\n        \"\"\"\n        logger.debug(\"Fetching DEP account information\")\n        res = self.send(requests.Request(\"GET\", self._url + \"/account\"))\n        return res.json()\n\n    def fetch_devices(self, cursor: Union[str, None] = None, limit: int = 100) -> dict:\n        \"\"\"Fetch a list of DEP devices\n\n        Args:\n              cursor (str): The cursor from the last fetch (must be younger than 7 days).\n              limit (int): Limit the number of records in the response. Default is 100\n        Returns:\n              dict: Response as per the sync devices documentation.\n        \"\"\"\n        req = requests.Request(\"POST\", self._url + \"/server/devices\", json={'limit': limit, 'cursor': cursor})\n        res = self.send(req)\n        return res.json()\n\n    def sync_devices(self, cursor: str, limit: int = 100) -> dict:\n        \"\"\"Fetch devices changed since the cursor was issued.\n\n        Args:\n              cursor (str): The cursor from the last sync (must be younger than 7 days).\n              limit (int): Limit the number of records in the response. Default is 100\n        Returns:\n              dict: Response as per the sync devices documentation.\n        \"\"\"\n        req = requests.Request(\"POST\", self._url + \"/devices/sync\", json={'limit': limit, 'cursor': cursor})\n        res = self.send(req)\n        return res.json()\n\n    def devices(self, cursor: Union[str, None] = None) -> Iterator:\n        \"\"\"Get an iterable object which calls fetch or sync to retrieve all device records.\n\n        Args:\n              cursor (str): If supplied, the cursor returned will perform the sync operation. Otherwise you will\n                receive a cursor that performs a fetch for each iteration, until the fetch cursor is exhausted.\n\n        Returns:\n              Union[DEPSyncCursor, DEPFetchCursor]: A cursor that is iterable\n        \"\"\"\n        if cursor is not None:  # Could actually be an expired cursor here\n            return DEPSyncCursor(self, cursor=cursor)\n        else:\n            return DEPFetchCursor(self)\n\n    def device_detail(self, *serial_numbers: Union[str, List[str]]):\n        \"\"\"Fetch detail about a list of devices\n\n        Args:\n              serial_numbers (List[str]): A list of device serial numbers to fetch details for.\n\n        Returns:\n              dict: Device information\n        \"\"\"\n        req = requests.Request(\"POST\", self._url + \"/devices\", json={'devices': serial_numbers})\n        res = self.send(req)\n        return res.json()\n\n    def define_profile(self, profile: dict):\n        \"\"\"Define a DEP profile\n\n        Args:\n              profile (dict): A DEP profile.\n\n        \"\"\"\n        req = requests.Request(\"POST\", self._url + \"/profile\", json=profile)\n        res = self.send(req)\n        return res.json()\n\n    def assign_profile(self, profile_uuid: str, *serial_numbers: List[str]) -> dict:\n        \"\"\"Assign an existing profile to device(s)\n\n        Args:\n              profile_uuid (str): The UUID of the profile to assign.\n              serial_numbers (List[str]): A list of serial numbers to assign to that profile.\n\n        Returns:\n              dict: Assignment information\n        \"\"\"\n        req = requests.Request(\"POST\", self._url + \"/profile/devices\",\n                               json={'profile_uuid': profile_uuid, 'devices': serial_numbers})\n        res = self.send(req)\n        return res.json()\n\n    def remove_profile(self, *serial_numbers: List[str]) -> DEPProfileRemovals:\n        \"\"\"Unassign all profiles from device(s)\n\n        Args:\n              serial_numbers (List[str]): A list of serial numbers to unassign from that profile.\n\n        Returns:\n              dict: Assignment information\n        \"\"\"\n        req = requests.Request(\"DELETE\", self._url + \"/profile/devices\",\n                               json={'devices': serial_numbers})\n        res = self.send(req)\n        return res.json()\n\n    def profile(self, uuid: str) -> dict:\n        \"\"\"Get an existing profile by its UUID.\n\n        Args:\n              uuid (str): Profile UUID\n\n        Returns:\n              dict: Profile\n        \"\"\"\n        params = {'profile_uuid': uuid} if uuid is not None else None\n        req = requests.Request(\"GET\", self._url + \"/profile\", params=params)\n        res = self.send(req)\n        return res.json()\n\n    def activation_lock(self,\n                        serial_number: str,\n                        escrow_key: Optional[str] = None,\n                        lost_message: Optional[str] = None):\n        \"\"\"Lock a device with Activation Lock.\"\"\"\n        pass\n\n    def activation_lock_bypass(self,\n                               serial_number: str,\n                               product_type: str,\n                               org_name: str,\n                               guid: str,\n                               escrow_key: str,\n                               imei: Optional[str] = None,\n                               meid: Optional[str] = None):\n        \"\"\"Remove Activation Lock from a device.\"\"\"\n        pass\n\n    def disown(self, *serial_numbers: List[str]):\n        \"\"\"Disown devices.\n\n        This action is PERMANENT (except in the case of iPads added via Apple Configurator 2).\n        \"\"\"\n        pass\n\n\nclass DEPBaseCursor(object):\n    \"\"\"DEPCursor is the base class for DEP Fetch and Sync cursors.\n\n    Attributes:\n          owner (DEP): The DEP instance that created this iterator.\n          results (dict): The current response results.\n    \"\"\"\n\n    def __init__(self, owner: DEP, results: Optional[dict] = None) -> None:\n        self.owner = owner\n        self.results = results\n\n    @property\n    def cursor(self) -> Optional[str]:\n        if not self.results:\n            return None\n        return self.results.get('cursor', None)\n\n    @property\n    def more_to_follow(self) -> bool:\n        if not self.results:\n            return True\n        return self.results.get('more_to_follow', False)\n\n    def __iter__(self):\n        return self\n\n\nclass DEPFetchCursor(DEPBaseCursor, Iterator):\n    \"\"\"DEPFetchCursor wraps the DEP device fetch cursor as an iterable object.\"\"\"\n    def __next__(self):\n        if not self.more_to_follow:\n            raise StopIteration()\n\n        if self.cursor is None:\n            self.results = self.owner.fetch_devices()\n        else:\n            self.results = self.owner.fetch_devices(cursor=self.cursor)\n\n        return self.results\n\n\nclass DEPSyncCursor(DEPBaseCursor, Iterator):\n    \"\"\"DEPSyncCursor wraps the DEP device sync cursor as an iterable object.\"\"\"\n    def __init__(self, owner: DEP, cursor: str, results: Optional[dict] = None) -> None:\n        super(DEPSyncCursor, self).__init__(owner, results)\n        self.results = {'cursor': cursor, 'more_to_follow': True}\n\n    def __next__(self):\n        if not self.more_to_follow:\n            raise StopIteration()\n\n        self.results = self.owner.sync_devices(cursor=self.cursor)\n\n        return self.results\n"
  },
  {
    "path": "commandment/dep/errors.py",
    "content": "from requests import Response, HTTPError\n\n\nclass DEPServiceError(HTTPError):\n    \"\"\"DEPServiceError inherits from request's HTTPError to provide the response and request as part of the exception.\n\n    Additionally, the error tracks information about the body content as this can sometimes be the only way to\n    distinguish an error.\n\n    Attributes:\n          text (str): The reserved string that was returned in the error body.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super(DEPServiceError, self).__init__(*args, **kwargs)\n        if 'response' in kwargs:\n            # Quote characters (\") must be stripped, because the body may contain the reason inside double quotes.\n            self.text = kwargs.get('response').content.decode('utf8').strip(\"\\\"\\n\\r\")\n        else:\n            self.text = \"NO_REASON_GIVEN\"\n\n    def __str__(self):\n        return '{}: {}'.format(self.response.status_code, self.text)\n\n\nclass DEPClientError(Exception):\n    \"\"\"DEPClientError describes errors that happen on the client side, often as a result of failed validations.\"\"\"\n    pass\n"
  },
  {
    "path": "commandment/dep/models.py",
    "content": "from cryptography import x509\nfrom commandment.dep import SkipSetupSteps, DEPOrgType, DEPOrgVersion, SetupAssistantStep\nfrom commandment.models import db\nfrom commandment.mutablelist import MutableList\nfrom commandment.pki.models import CertificateType, Certificate\nfrom commandment.dbtypes import GUID, JSONEncodedDict, SetOfEnumValues\n\n\nclass DEPServerTokenCertificate(Certificate):\n    \"\"\"DEP Server Token Certificate\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.STOKEN.value\n    }\n\n    @classmethod\n    def from_crypto(cls, certificate: x509.Certificate):\n        m = Certificate.from_crypto_type(certificate, CertificateType.STOKEN)\n        return m\n\n\nclass DEPAnchorCertificate(Certificate):\n    \"\"\"DEP Anchor Certificate\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.ANCHOR.value\n    }\n\n\nclass DEPSupervisionCertificate(Certificate):\n    \"\"\"DEP Supervision Certificate\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.SUPERVISION.value\n    }\n\n\nclass DEPAccount(db.Model):\n    \"\"\"DEP Account\n\n    This table stores information about a single DEP account (aka one 'MDM Server' in the portal),\n     and its current token.\n    \"\"\"\n    __tablename__ = 'dep_accounts'\n\n    id = db.Column(db.Integer, primary_key=True)\n\n    # certificate for PKI of server token\n    certificate_id = db.Column(db.ForeignKey('certificates.id'))\n    certificate = db.relationship('DEPServerTokenCertificate', backref='dep_configurations')\n\n    # OAuth creds\n    consumer_key = db.Column(db.String())\n    consumer_secret = db.Column(db.String())\n    access_token = db.Column(db.String())\n    access_secret = db.Column(db.String())\n    access_token_expiry = db.Column(db.DateTime())\n\n    token_updated_at = db.Column(db.DateTime())\n\n    # Current session token\n    auth_session_token = db.Column(db.String())\n\n    # Information synchronised from the /account endpoint\n    server_name = db.Column(db.String())\n    server_uuid = db.Column(GUID)\n    admin_id = db.Column(db.String())\n    facilitator_id = db.Column(db.String())\n    org_name = db.Column(db.String())\n    org_email = db.Column(db.String())\n    org_phone = db.Column(db.String())\n    org_address = db.Column(db.String())\n    org_type = db.Column(db.Enum(DEPOrgType))\n    org_version = db.Column(db.Enum(DEPOrgVersion))\n    org_id = db.Column(db.String())\n    org_id_hash = db.Column(db.String())\n\n    url = db.Column(db.String())\n\n    # Hold the state of the in-progress fetch/sync in case the DEP thread dies\n    cursor = db.Column(db.String())\n    more_to_follow = db.Column(db.Boolean())\n    fetched_until = db.Column(db.DateTime())\n\n    default_dep_profile_id = db.Column(db.Integer, db.ForeignKey('dep_profiles.id'))\n    default_dep_profile = db.relationship('DEPProfile', backref='default_for_accounts',\n                                          foreign_keys=[default_dep_profile_id])\n\n\n\ndep_profile_anchor_certificates = db.Table(\n    'dep_profile_anchor_certificates',\n    db.metadata,\n    db.Column('dep_profile_id', db.Integer, db.ForeignKey('dep_profiles.id')),\n    db.Column('certificate_id', db.Integer, db.ForeignKey('certificates.id')),\n)\n\ndep_profile_supervision_certificates = db.Table(\n    'dep_profile_supervision_certificates',\n    db.metadata,\n    db.Column('dep_profile_id', db.Integer, db.ForeignKey('dep_profiles.id')),\n    db.Column('certificate_id', db.Integer, db.ForeignKey('certificates.id')),\n)\n\n\nclass DEPProfile(db.Model):\n    __tablename__ = 'dep_profiles'\n    id = db.Column(db.Integer, primary_key=True)\n    uuid = db.Column(GUID, index=True)\n\n    # A profile is defined under a single DEP account\n    dep_account_id = db.Column(db.Integer, db.ForeignKey('dep_accounts.id'))\n    dep_account = db.relationship('DEPAccount', backref='dep_profiles', foreign_keys=[dep_account_id])\n\n    profile_name = db.Column(db.String, nullable=False)\n    url = db.Column(db.String, nullable=False)\n    allow_pairing = db.Column(db.Boolean, default=True)\n    is_supervised = db.Column(db.Boolean, default=False)\n    is_multi_user = db.Column(db.Boolean, default=False)\n    is_mandatory = db.Column(db.Boolean, default=False)\n    await_device_configured = db.Column(db.Boolean, default=False)\n    is_mdm_removable = db.Column(db.Boolean, default=True)\n    support_phone_number = db.Column(db.String)\n    auto_advance_setup = db.Column(db.Boolean, default=False)\n    support_email_address = db.Column(db.String)\n    org_magic = db.Column(db.String)\n    skip_setup_items = db.Column(SetOfEnumValues(SetupAssistantStep))\n    department = db.Column(db.String)\n    # language = db.Column(db.String)\n    # region = db.Column(db.String)\n    # last_upload_at = db.Column(db.DateTime)\n\n    anchor_certs = db.relationship(\n        'DEPAnchorCertificate',\n        secondary=dep_profile_anchor_certificates,\n        #  back_populates='anchor_dep_profiles'\n    )\n\n    supervising_host_certs = db.relationship(\n        'DEPSupervisionCertificate',\n        secondary=dep_profile_supervision_certificates,\n        #  back_populates='supervising_dep_profiles'\n    )\n"
  },
  {
    "path": "commandment/dep/resources.py",
    "content": "from flask import url_for\nfrom flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship\nfrom .schema import DEPProfileSchema, DEPAccountSchema\nfrom .models import db, DEPProfile, DEPAccount\n\n\nclass DEPProfileList(ResourceList):\n    schema = DEPProfileSchema\n    data_layer = {\n        'session': db.session,\n        'model': DEPProfile,\n    }\n\n    def before_post(self, args, kwargs, data=None):\n        \"\"\"Generate an MDM enrollment URL if none was given.\"\"\"\n        if 'url' not in data or data['url'] is None:\n            data['url'] = url_for('dep_app.profile', _external=True)\n\n\nclass DEPProfileDetail(ResourceDetail):\n    schema = DEPProfileSchema\n    data_layer = {\n        'session': db.session,\n        'model': DEPProfile,\n        'url_field': 'dep_profile_id'\n    }\n\n\nclass DEPProfileRelationship(ResourceRelationship):\n    schema = DEPProfileSchema\n    data_layer = {\n        'session': db.session,\n        'model': DEPProfile,\n        'url_field': 'dep_profile_id'\n    }\n\n\nclass DEPAccountList(ResourceList):\n    schema = DEPAccountSchema\n    data_layer = {\n        'session': db.session,\n        'model': DEPAccount,\n    }\n\n\nclass DEPAccountDetail(ResourceDetail):\n    schema = DEPAccountSchema\n    data_layer = {\n        'session': db.session,\n        'model': DEPAccount,\n        'url_field': 'dep_account_id'\n    }\n"
  },
  {
    "path": "commandment/dep/schema.py",
    "content": "from flask import url_for\nfrom marshmallow_jsonapi.flask import Relationship, Schema\nfrom marshmallow_jsonapi import fields\nfrom marshmallow_enum import EnumField\nfrom . import SetupAssistantStep\n\n\nclass DEPProfileSchema(Schema):\n    \"\"\"marshmallow schema for a DEP profile.\n\n    See Also:\n        - `/profile endpoint <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/4-Profile_Management/ProfileManagement.html#//apple_ref/doc/uid/TP40017387-CH7-SW6>`_.\n        - `Mobile Device Management Protocol Reference <https://developer.apple.com/enterprise/documentation/MDM-Protocol-Reference.pdf>`_ \"Define Profile\" pg. 120\n    \"\"\"\n    class Meta:\n        type_ = 'dep_profiles'\n        self_view = 'dep_app.dep_profile_detail'\n        self_view_kwargs = {'dep_profile_id': '<id>'}\n        self_view_many = 'dep_app.dep_profiles_list'\n        strict = True\n\n    id = fields.Int(dump_only=True)\n    uuid = fields.UUID(dump_only=True)\n    \"\"\"str: The Apple assigned UUID of this DEP Profile\"\"\"\n\n    profile_name = fields.String(required=True)\n    \"\"\"str: A human-readable name for the profile.\"\"\"\n    url = fields.Url(required=False)  # Should be required\n    \"\"\"str: The URL of the MDM server.\"\"\"\n    allow_pairing = fields.Boolean(default=True)\n    \"\"\"bool: If true, any device can pair with this device, supervision certs are not required.\"\"\"\n    is_supervised = fields.Boolean(default=False)\n    \"\"\"bool: If true, the device must be supervised\"\"\"\n    is_multi_user = fields.Boolean(default=False)\n    \"\"\"bool: If true, tells the device to configure for Shared iPad.\"\"\"\n    is_mandatory = fields.Boolean(default=False)\n    \"\"\"bool: If true, the user may not skip applying the profile returned by the MDM server\"\"\"\n    await_device_configured = fields.Boolean()\n    \"\"\"bool: If true, Setup Assistant does not continue until the MDM server sends DeviceConfigured.\"\"\"\n    is_mdm_removable = fields.Boolean()\n    \"\"\"bool: If false, the MDM payload delivered by the configuration URL cannot be removed by the user via the user \n    interface on the device\"\"\"\n    support_phone_number = fields.String(allow_none=True)\n    \"\"\"str: A support phone number for the organization.\"\"\"\n    auto_advance_setup = fields.Boolean()\n    \"\"\"bool: If set to true, the device will tell tvOS Setup Assistant to automatically advance though its screens.\"\"\"\n    support_email_address = fields.String(allow_none=True)  # No need to perform validation here\n    \"\"\"str: A support email address for the organization.\"\"\"\n    org_magic = fields.String(allow_none=True)\n    \"\"\"str: A string that uniquely identifies various services that are managed by a single organization.\"\"\"\n    anchor_certs = fields.List(fields.String())\n    \"\"\"List[str]: Each string should contain a DER-encoded certificate converted to Base64 encoding. If provided, \n    these certificates are used as trusted anchor certificates when evaluating the trust of the connection \n    to the MDM server URL.\"\"\"\n    supervising_host_certs = fields.List(fields.String())\n    \"\"\"List[str]: Each string contains a DER-encoded certificate converted to Base64 encoding. If provided, \n    the device will continue to pair with a host possessing one of these certificates even when allow_pairing \n    is set to false\"\"\"\n    skip_setup_items = fields.List(EnumField(SetupAssistantStep))\n    \"\"\"Set[SetupAssistantStep]: A list of setup panes to skip\"\"\"\n    department = fields.String(allow_none=True)\n    \"\"\"str: The user-defined department or location name.\"\"\"\n    last_upload_at = fields.DateTime(dump_only=True)\n    \"\"\"datetime: The last time this profile was uploaded/synced to apple. null if it was never synced.\"\"\"\n\n    devices = Relationship(\n        related_view='api_app.devices_list',\n        related_view_kwargs={'dep_profile_id': '<id>'},\n        many=True, include_resource_linkage=True,\n        schema='DeviceSchema',\n        type_='devices'\n    )\n\n    dep_account = Relationship(\n        self_view='dep_app.dep_profile_dep_account',\n        self_view_kwargs={'dep_profile_id': '<id>'},\n        related_view='dep_app.dep_account_detail',\n        related_view_kwargs={'dep_account_id': '<dep_account_id>'},\n        many=False, include_resource_linkage=True,\n        schema='DEPAccountSchema',\n        type_='dep_accounts'\n    )\n\n\nclass DEPDeviceSchema(Schema):\n    \"\"\"The Device dictionary returned by the DEP Devices fetch endpoint.\n\n    See Also:\n          https://mdmenrollment.apple.com/server/devices\n    \"\"\"\n    serial_number = fields.String()\n    model = fields.String()\n    description = fields.String()\n    color = fields.String()\n    asset_tag = fields.String()\n    profile_status = fields.String()\n    profile_uuid = fields.UUID()\n    profile_assign_time = fields.DateTime()\n    profile_push_time = fields.DateTime()\n    device_assigned_date = fields.DateTime()\n    device_assigned_by = fields.Email()\n    os = fields.String()\n    device_family = fields.String()\n\n\nclass DEPDeviceSyncSchema(Schema):\n    \"\"\"The device dictionary returned by the DEP Devices sync endpoint.\n\n    This adds the operation type and date.\"\"\"\n    op_type = fields.String()\n    op_date = fields.DateTime()\n    \n\nclass DEPDeviceCursorSchema(Schema):\n    \"\"\"The response JSON literal of the device fetch endpoint\"\"\"\n    cursor = fields.String()\n    more_to_follow = fields.Boolean()\n    devices = fields.Nested(DEPDeviceSchema, many=True)\n    fetched_until = fields.DateTime()\n\n\nclass MDMServiceURL(Schema):\n    uri = fields.URL()\n    http_method = fields.String()\n    # limit\n\n\nclass DEPAccountSchema(Schema):\n    \"\"\"DEP Account Details\"\"\"\n    class Meta:\n        type_ = 'dep_accounts'\n        self_view = 'dep_app.dep_account_detail'\n        self_view_kwargs = {'dep_account_id': '<id>'}\n        self_view_many = 'dep_app.dep_accounts_list'\n        strict = True\n\n    id = fields.Int(dump_only=True)\n\n    # stoken\n    consumer_key = fields.String()\n    consumer_secret = fields.String(load_only=True)\n    access_token = fields.String()\n    access_secret = fields.String(load_only=True)\n    access_token_expiry = fields.DateTime(dump_only=True)\n    token_updated_at = fields.DateTime(dump_only=True)\n    auth_session_token = fields.String(load_only=True)\n\n    # org\n    server_name = fields.String()\n    server_uuid = fields.UUID()\n    admin_id = fields.String()\n    facilitator_id = fields.String()\n    org_name = fields.String()\n    org_email = fields.Email()\n    org_phone = fields.String()\n    org_address = fields.String()\n    # urls = fields.Nested(MDMServiceURL, many=True)\n    org_type = fields.String()\n    org_version = fields.String()\n    org_id = fields.String()\n    org_id_hash = fields.String()\n    url = fields.String()\n\n    cursor = fields.String()\n    more_to_follow = fields.Boolean()\n    fetched_until = fields.DateTime()\n\n    dep_profiles = Relationship(\n        related_view='dep_app.dep_profile_detail',\n        related_view_kwargs={'dep_profile_id': '<id>'},\n        many=True, include_resource_linkage=True,\n        schema='DEPProfileSchema',\n        type_='dep_profiles'\n    )\n"
  },
  {
    "path": "commandment/dep/smime.py",
    "content": "from typing import Optional\nimport email\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.asymmetric import rsa, padding\nfrom cryptography.hazmat.primitives.ciphers.algorithms import TripleDES, AES\nfrom cryptography.hazmat.primitives.ciphers import Cipher, modes\nfrom email.message import Message\nfrom base64 import b64decode\nfrom asn1crypto.cms import EnvelopedData, ContentInfo, RecipientInfo, IssuerAndSerialNumber, KeyTransRecipientInfo, \\\n    RecipientIdentifier, EncryptionAlgorithm\n\n\ndef decrypt(smime: bytes, key: rsa.RSAPrivateKey, serial: Optional[int] = None):\n    \"\"\"Decrypt an S/MIME message using the RSA Private Key given.\n\n    The recipient can be hinted using the serial parameter, otherwise we assume single recipient = the given key.\n    \"\"\"\n    string_content = smime.decode('utf8')\n    msg: Message = email.message_from_string(string_content)\n    assert msg.get_content_type() == 'application/pkcs7-mime'\n    assert msg.get_filename() == 'smime.p7m'\n    assert msg.get('Content-Description') == 'S/MIME Encrypted Message'\n\n    b64payload = msg.get_payload()\n    payload = b64decode(b64payload)\n    decrypted_data = decrypt_smime_content(payload, key)\n    decrypted_msg: Message = email.message_from_bytes(decrypted_data)\n\n    return decrypted_msg.get_payload()\n\n\ndef decrypt_smime_content(payload: bytes, key: rsa.RSAPrivateKey) -> bytes:\n    content_info = ContentInfo.load(payload)\n\n    assert content_info['content_type'].native == 'enveloped_data'\n    content: EnvelopedData = content_info['content']\n\n    matching_recipient = content['recipient_infos'][0]\n\n    # Need to see if we hold the key for any valid recipient.\n    # for recipient_info in content['recipient_infos']:\n    #     assert recipient_info.name == 'ktri'  # Only support KeyTransRecipientInfo\n    #     ktri: KeyTransRecipientInfo = recipient_info.chosen\n    #     recipient_id: RecipientIdentifier = ktri['rid']\n    #     assert recipient_id.name == 'issuer_and_serial_number'  # Only support IssuerAndSerialNumber\n    #     matching_recipient = recipient_info\n\n    encryption_algo = matching_recipient.chosen['key_encryption_algorithm'].native\n    encrypted_key = matching_recipient.chosen['encrypted_key'].native\n\n    assert encryption_algo['algorithm'] == 'rsa'\n\n    # Get the content key\n    plain_key = key.decrypt(\n        encrypted_key,\n        padding=padding.PKCS1v15(),\n    )\n\n    # Now we have the plain key, we can decrypt the encrypted data\n    encrypted_contentinfo = content['encrypted_content_info']\n\n    algorithm: EncryptionAlgorithm = encrypted_contentinfo['content_encryption_algorithm']  #: EncryptionAlgorithm\n    encrypted_content_bytes = encrypted_contentinfo['encrypted_content'].native\n\n    symkey = None\n\n    if algorithm.encryption_cipher == 'aes':\n        symkey = AES(plain_key)\n    elif algorithm.encryption_cipher == 'tripledes':\n        symkey = TripleDES(plain_key)\n    else:\n        print('Dont understand encryption cipher: ', algorithm.encryption_cipher)\n\n    cipher = Cipher(symkey, modes.CBC(algorithm.encryption_iv), backend=default_backend())\n    decryptor = cipher.decryptor()\n\n    decrypted_data = decryptor.update(encrypted_content_bytes) + decryptor.finalize()\n    return decrypted_data\n"
  },
  {
    "path": "commandment/dep/threads.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2018 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\nAttributes:\n    dep_thread (threading.Timer):\n    dep_start (int): In seconds, time of first run\n    dep_time (int): In seconds, time of subsequent runs\n\nTodo:\n    * Currently we start this thread after the database context and\n      configuration has already been. We envision a day when this runner runs\n      standalone and thus we'll need to sort out separate configuration routines etc.\n\"\"\"\nimport logging\nimport threading\nimport datetime\nimport dateutil.parser\nfrom flask import Flask\n\n# Necessary because SQLAlchemy isn't threadsafe by default\nfrom sqlalchemy.orm import scoped_session\nfrom sqlalchemy.orm import sessionmaker\n\nfrom commandment.dep.apple_schema import AppleDEPProfileSchema\nfrom commandment.dep.errors import DEPServiceError\nfrom commandment.models import db, Device\nfrom commandment.dep.models import DEPAccount, DEPProfile\nfrom commandment.dep.dep import DEP\nfrom commandment.dep import DEPOrgType, DEPOrgVersion, DEPOperationType\nimport sqlalchemy.orm.exc\nimport sqlalchemy.exc\n\ndep_thread = None\ndep_start = 5\ndep_time = 90\n\nlogger = logging.getLogger('dep thread')\n\n\ndef start(app: Flask):\n    \"\"\"Start the StartUp thread\"\"\"\n    logger.info('DEP thread will run in 5 seconds')\n    dep_thread = threading.Timer(dep_start, dep_thread_callback, [app])\n    dep_thread.daemon = True\n    dep_thread.start()\n\n\ndef stop():\n    \"\"\"Stop the runner thread\"\"\"\n    logger.info('DEP thread will stop')\n    global dep_thread\n    if dep_thread is threading.Timer:\n        dep_thread.cancel()\n\n\ndef dep_sync_organization(app: Flask, dep: DEP):\n    \"\"\"Synchronise information from the DEP service to the local database.\n    \"\"\"\n    with app.app_context():\n        try:\n            app.logger.debug('Querying for DEP Account information from the database')\n            dep_account: DEPAccount = db.session.query(DEPAccount).one()\n\n            # Refresh organisation information if there is none\n            if dep_account.server_name is None or dep_account.server_uuid is None:\n                app.logger.debug('Refreshing information about organization from the DEP service')\n                account = dep.account()\n\n                if account is not None:\n                    dep_account.server_uuid = account.get('server_uuid', None)\n                    dep_account.server_name = account.get('server_name', None)\n                    dep_account.facilitator_id = account.get('facilitator_id', None)\n                    dep_account.admin_id = account.get('admin_id', None)\n                    dep_account.org_name = account.get('org_name', None)\n                    dep_account.org_email = account.get('org_email', None)\n                    dep_account.org_phone = account.get('org_phone', None)\n                    dep_account.org_address = account.get('org_address', None)\n                    dep_account.org_id = account.get('org_id', None)\n                    dep_account.org_id_hash = account.get('org_id_hash', None)\n                    if 'org_type' in account:\n                        dep_account.org_type = DEPOrgType(account['org_type'])\n\n                    if 'org_version' in account:\n                        dep_account.org_version = DEPOrgVersion(account['org_version'])\n\n                    db.session.commit()\n                    app.logger.info('Successfully fetched DEP Organization: %s', dep_account.org_name)\n                else:\n                    app.logger.warn('Failed to fetch DEP Organization')\n            else:\n                app.logger.info('DEP Organization already fetched: %s', dep_account.org_name)\n\n        except sqlalchemy.orm.exc.NoResultFound:\n            app.logger.info('Not attempting to fetch DEP account information. No DEP account is configured.')\n\n\ndef dep_fetch_devices(app: Flask, dep: DEP, dep_account_id: int):\n    \"\"\"Perform fetch or sync of devices.\n\n    TODO: If default DEP Profile is nominated, it is queued for assignment here. But may want to check `profile_status`\n        to see whether only devices with the `removed` status are considered unassigned.\n\n    See:\n        https://docs.sqlalchemy.org/en/latest/orm/contextual.html\n    \"\"\"\n    thread_session = db.create_scoped_session()\n\n    dep_account: DEPAccount = thread_session.query(DEPAccount).one()\n\n    if dep_account.cursor is not None:\n        app.logger.info('Syncing using previous cursor: %s', dep_account.cursor)\n    else:\n        app.logger.info('No DEP cursor found, performing a full fetch')\n\n    # TODO: if fetched_until is quite recent, there's no reason to fetch again\n    for device_page in dep.devices(dep_account.cursor):\n        print(device_page)\n        for device in device_page['devices']:\n            if 'op_type' in device:  # its a sync, not a fetch\n                optype = DEPOperationType(device['op_type'])\n\n                if optype == DEPOperationType.Added:\n                    app.logger.debug('DEP Added: %s', device['serial_number'])\n                elif optype == DEPOperationType.Modified:\n                    app.logger.debug('DEP Modified: %s', device['serial_number'])\n                elif optype == DEPOperationType.Deleted:\n                    app.logger.debug('DEP Deleted: %s', device['serial_number'])\n                else:\n                    app.logger.error('DEP op_type not recognised (%s), skipping', device['op_type'])\n                    continue\n            else:\n                pass\n\n            try:\n                d: Device = thread_session.query(Device).filter(Device.serial_number == device['serial_number']).one()\n                d.description = device['description']\n                d.model = device['model']\n                d.os = device['os']\n                d.device_family = device['device_family']\n                d.color = device['color']\n                d.profile_status = device['profile_status']\n                if device['profile_status'] != 'empty':\n                    d.profile_uuid = device.get('profile_uuid', None)  # Only exists in DEP Sync not Fetch?\n                    d.profile_assign_time = dateutil.parser.parse(device['profile_assign_time'])\n\n                d.device_assigned_by = device['device_assigned_by']\n                d.device_assigned_date = dateutil.parser.parse(device['device_assigned_date'])\n                d.is_dep = True\n\n            except sqlalchemy.orm.exc.NoResultFound:\n                app.logger.debug('No existing device record for serial: %s', device['serial_number'])\n\n                if device['profile_status'] != 'empty':\n                    device['profile_assign_time'] = dateutil.parser.parse(device['profile_assign_time'])\n\n                device['device_assigned_date'] = dateutil.parser.parse(device['device_assigned_date'])\n\n                if 'op_type' in device:\n                    del device['op_type']\n                    del device['op_date']\n                    del device['profile_assign_time']\n                    del device['device_assigned_date']\n\n                d = Device(**device)\n                d.is_dep = True\n                thread_session.add(d)\n\n            except sqlalchemy.exc.StatementError as e:\n                app.logger.error('Got a statement error trying to insert a DEP device: {}'.format(e))\n\n        app.logger.debug('Last DEP Cursor was: %s', device_page['cursor'])\n        dep_account.cursor = device_page.get('cursor', None)\n        dep_account.more_to_follow = device_page.get('more_to_follow', None)\n        dep_account.fetched_until = dateutil.parser.parse(device_page['fetched_until'])\n        thread_session.commit()\n\n\ndef dep_define_profiles(app: Flask, dep: DEP):\n    \"\"\"Create DEP profiles which have not yet been synced with Apple.\"\"\"\n    thread_session = db.create_scoped_session()\n\n    dep_profiles_pending = thread_session.query(DEPProfile).filter(\n        DEPProfile.uuid.is_(None), DEPProfile.last_upload_at.is_(None)).all()\n    app.logger.debug('There are %d pending DEP profile(s) to upload', len(dep_profiles_pending))\n\n    for dep_profile in dep_profiles_pending:\n        try:\n            schema = AppleDEPProfileSchema()\n            dep_profile_apple = schema.dump(dep_profile)\n            print(dep_profile_apple.data)\n            response = dep.define_profile(dep_profile_apple.data)\n            assert 'profile_uuid' in response\n            dep_profile.uuid = response['profile_uuid']\n            dep_profile.last_uploaded_at = datetime.datetime.now()\n        except Exception as e:\n            app.logger.error('Got an exception trying to define a profile: {}'.format(e))\n\n    thread_session.commit()\n\n\ndef dep_thread_callback(app: Flask):\n    \"\"\"Runner thread main procedure\n\n    Todo:\n        * Catch everything so we don't interrupt the thread (and it never reschedules)\n        * Certificate expiration warnings/emails\n    \"\"\"\n    threadlocals = threading.local()\n\n    with app.app_context():\n        try:\n            dep_account: DEPAccount = db.session.query(DEPAccount).one()\n            app.logger.info('Checking DEP state')\n\n            dep = DEP(\n                consumer_key=dep_account.consumer_key,\n                consumer_secret=dep_account.consumer_secret,\n                access_token=dep_account.access_token,\n                access_secret=dep_account.access_secret,\n            )\n\n            dep_sync_organization(app, dep)\n\n            try:\n                dep_fetch_devices(app, dep, dep_account.id)\n            except DEPServiceError as dse:\n                print(dse)\n                if dse.text == 'EXPIRED_CURSOR':\n                    app.logger.info(\"Sync cursor had expired, clearing for next run...\")\n                    dep_account.cursor = None\n                    db.session.add(dep_account)\n                    db.session.commit()\n\n            dep_define_profiles(app, dep)\n\n        except sqlalchemy.orm.exc.NoResultFound:\n            app.logger.info('Not attempting a DEP sync, no account configured.')\n\n"
  },
  {
    "path": "commandment/deprecated/models.py",
    "content": "from enum import Enum\n\nfrom sqlalchemy import Column, Integer, String, ForeignKey, Table, Text, Boolean, DateTime, Enum as DBEnum, text, \\\n    BigInteger, and_, or_, LargeBinary\nfrom sqlalchemy.orm import relationship\n\nfrom commandment.profiles.ad import ADMountStyle, ADNamespace, ADPacketSignPolicy, ADPacketEncryptPolicy, \\\n    ADCertificateAcquisitionMechanism\nfrom commandment.profiles.email import EmailAuthenticationType, EmailAccountType\nfrom commandment.profiles.vpn import VPNType\nfrom commandment.profiles.wifi import WIFIEncryptionType, WIFIProxyType\nfrom ..dbtypes import GUID, JSONEncodedDict\nfrom uuid import uuid4\nfrom .cert import KeyUsage\nfrom . import PayloadScope\n\nfrom ..models import db\n\npayload_dependencies = Table('payload_dependencies', db.metadata,\n                             Column('payload_uuid', GUID, ForeignKey('payloads.uuid')),\n                             Column('depends_on_payload_uuid', GUID, ForeignKey('payloads.uuid')),\n                             )\n\n\n\n\nclass Payload(db.Model):\n    __tablename__ = 'payloads'\n\n    id = Column(Integer, primary_key=True)\n    type = Column(String, index=True, nullable=False)\n    version = Column(Integer)\n    identifier = Column(String)\n    uuid = Column(GUID, index=True, default=uuid4(), nullable=False)\n    display_name = Column(String)\n    description = Column(Text)\n    organization = Column(String)\n\n    # Dependencies should be tracked in cases where the payload refers to another required payload.\n    # eg. a reference to certificate payload in an 802.1x configuration.\n    # depends_on = relationship(\"Payload\",\n    #                           secondary=payload_dependencies,\n    #                           backref=\"dependents\")\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'payload',\n        'polymorphic_on': type,\n    }\n\n\n\n\n\nclass ADCertPayload(Payload):\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    certificate_description = Column(String)  # Description was reserved from the base Payload table\n    allow_all_apps_access = Column(Boolean)\n    cert_server = Column(String, nullable=False)\n    cert_template = Column(String, nullable=False, default='User')\n    acquisition_mechanism = Column(DBEnum(ADCertificateAcquisitionMechanism), default=ADCertificateAcquisitionMechanism.RPC)\n    certificate_authority = Column(String, nullable=False)\n    renewal_time_interval = Column(Integer)\n    identity_description = Column(String, nullable=True)\n    key_is_extractable = Column(Boolean, default=False)\n    prompt_for_credentials = Column(Boolean)\n    keysize = Column(Integer, default=2048)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.ADCertificate.managed',\n    }\n\n\nclass ADPayload(Payload):\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    host_name = Column(String, nullable=False)\n    user_name = Column(String, nullable=False)\n    password = Column(String, nullable=False)\n    ad_organizational_unit = Column(String, nullable=False)\n    ad_mount_style = Column(DBEnum(ADMountStyle), nullable=False)\n    ad_default_user_shell = Column(String)\n    ad_map_uid_attribute = Column(String)\n    ad_map_gid_attribute = Column(String)\n    ad_map_ggid_attribute = Column(String)\n    ad_preferred_dc_server = Column(String)\n    ad_domain_admin_group_list = Column(String) # JSON\n    ad_namespace = Column(DBEnum(ADNamespace), default=ADNamespace.Domain)\n    ad_packet_sign = Column(DBEnum(ADPacketSignPolicy), default=ADPacketSignPolicy.Allow)\n    ad_packet_encrypt = Column(DBEnum(ADPacketEncryptPolicy), default=ADPacketEncryptPolicy.Allow)\n    ad_restrict_ddns = Column(String)  # JSON\n    ad_trust_change_pass_interval = Column(Integer)\n\n    # We will take null to mean that the flag is not set\n    ad_create_mobile_account_at_login = Column(Boolean)\n    ad_warn_user_before_creating_ma = Column(Boolean)\n    ad_force_home_local = Column(Boolean)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.DirectoryService.managed',\n    }\n\n\n# class EAPClientConfiguration(db.Model):\n#     __table__ = 'eap_client_configurations'\n#\n#     id = Column(Integer, primary_key=True)\n# accept_eap_types\n# payload_id = Column(Integer, ForeignKey('payloads.id'))\n#user_name = Column(String)\n#user_password = Column(String)\n#one_time_password = Column(Boolean)\n# payload_certificate_anchor_uuid\n# tls_trusted_server_names\n#tls_allow_trust_exceptions = Column(Boolean)\n#ttls_inner_authentication = Column(String)\n#outer_identity = Column(String)\n#system_mode_credentials_source = Column(String)\n#eap_fast_use_pac = Column(Boolean)\n#eap_fast_provision_pac = Column(Boolean)\n#eap_fast_provision_pac_anonymously = Column(Boolean)\n#eap_sim_number_of_rands = Column(Integer)\n\n\nclass WIFIPayload(Payload):\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    ssid_str = Column(String, nullable=False)\n    hidden_network = Column(Boolean, default=False)\n    auto_join = Column(Boolean, nullable=True)\n    encryption_type = Column(DBEnum(WIFIEncryptionType), default=WIFIEncryptionType.Any)\n    is_hotspot = Column(Boolean)\n    domain_name = Column(String)\n    service_provider_roaming_enabled = Column(Boolean)\n\n    roaming_consortium_ois = Column(String) # JSON\n    nai_realm_names = Column(String) # JSON\n    mccs_and_mncs = Column(String) # JSON\n    displayed_operator_name = Column(String)\n    captive_bypass = Column(Boolean)\n\n    # If WEP, WPA or Any\n    password = Column(String)\n    #eap_client_configuration_id = Column(Integer, ForeignKey('eap_client_configurations.id'))\n    tls_certificate_required = Column(Boolean)\n    payload_certificate_uuid = Column(GUID)\n\n    # Manual Proxy\n    proxy_type = Column(String)\n    proxy_server = Column(String)\n    proxy_server_port = Column(Integer)\n    proxy_username = Column(String)\n    proxy_password = Column(String)\n    proxy_pac_url = Column(String)\n    proxy_pac_fallback_allowed = Column(Boolean)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.wifi.managed',\n    }\n\n\nclass VPNPayload(Payload):\n    \"\"\"VPN Payload\"\"\"\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    user_defined_name = Column(String)\n    override_primary = Column(Boolean, default=False)\n    vpn_type = Column(DBEnum(VPNType), nullable=False)\n    vpn_sub_type = Column(String)\n    provider_bundle_identifier = Column(String)\n    on_demand_enabled = Column(Integer)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.vpn.managed',\n    }\n\n\nclass EmailPayload(Payload):\n    \"\"\"E-mail Payload\"\"\"\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    email_account_description = Column(String)\n    email_account_name = Column(String)\n    email_account_type = Column(DBEnum(EmailAccountType), nullable=False)\n    email_address = Column(String)\n    incoming_auth = Column(DBEnum(EmailAuthenticationType), nullable=False)\n    incoming_host = Column(String, nullable=False)\n    incoming_port = Column(Integer)\n    incoming_use_ssl = Column(Boolean, default=False)\n    incoming_username = Column(String, nullable=False)\n    incoming_password = Column(String)\n    outgoing_password = Column(String)\n    outgoing_incoming_same = Column(Boolean)\n    outgoing_auth = Column(DBEnum(EmailAuthenticationType), nullable=False)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.mail.managed'\n    }\n\n\nclass CertificatePayload(Payload):\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    certificate_file_name = Column(String)\n    payload_content = Column(LargeBinary)\n    password = Column(String)\n    __mapper_args__ = {\n        'polymorphic_identity': 'certificate'\n    }\n\n\nclass PEMCertificatePayload(CertificatePayload):\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.security.pem'\n    }\n\n\nclass DERCertificatePayload(CertificatePayload):\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.security.pkcs1'\n    }\n\n\nclass PKCS12CertificatePayload(CertificatePayload):\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.security.pkcs12'\n    }\n\n\nclass PasswordPolicyPayload(Payload):\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    allow_simple = Column(Boolean)\n    force_pin = Column(Boolean)\n    max_failed_attempts = Column(Integer)\n    max_inactivity = Column(Integer)\n    max_pin_age_in_days = Column(Integer)\n    min_complex_chars = Column(Integer)\n    min_length = Column(Integer)\n    require_alphanumeric = Column(Boolean)\n    pin_history = Column(Integer)\n    max_grace_period = Column(Integer)\n    allow_fingerprint_modification = Column(Boolean)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.mobiledevice.passwordpolicy'\n    }\n\n\nclass EnergySaverPayload(Payload):\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    destroy_fv_key_on_standby = Column(Boolean)\n    sleep_disabled = Column(Boolean)\n    desktop_acpower_profilenumber = Column(Integer)\n    portable_acpower_profilenumber = Column(Integer)\n    portable_battery_profilenumber = Column(Integer)\n    desktop_acpower = Column(JSONEncodedDict)\n    portable_acpower = Column(JSONEncodedDict)\n    portable_battery = Column(JSONEncodedDict)\n    desktop_schedule = Column(JSONEncodedDict)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.MCX'\n    }\n\n\nclass MDMPayload(Payload):\n    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)\n    identity_certificate_uuid = Column(GUID, nullable=False)\n    topic = Column(String, nullable=False)\n    server_url = Column(String, nullable=False)\n    server_capabilities = Column(String)\n    sign_message = Column(Boolean)\n    check_in_url = Column(String)\n    check_out_when_removed = Column(Boolean)\n    access_rights = Column(Integer)\n    use_development_apns = Column(Boolean)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.mdm'\n    }\n\n\nprofile_payloads = Table('profile_payloads', db.metadata,\n                         Column('profile_id', Integer, ForeignKey('profiles.id')),\n                         Column('payload_id', Integer, ForeignKey('payloads.id')))\n\nclass Profile(db.Model):\n    \"\"\"Top level profile.\n\n    In Commandment, multiple profiles may have an association with the same payload.\n\n    See Also:\n          - `Configuration Profile Keys\n            <https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010206-CH1-SW7>`_.\n\n    Attributes:\n\n    \"\"\"\n    __tablename__ = 'profiles'\n\n    id = Column(Integer, primary_key=True)\n    description = Column(Text)\n    display_name = Column(String)\n    expiration_date = Column(DateTime)  # Only for old style OTA\n    identifier = Column(String, nullable=False)\n    organization = Column(String)\n    uuid = Column(GUID, index=True, default=uuid4())\n    removal_disallowed = Column(Boolean)\n    version = Column(Integer, default=1)\n    scope = Column(DBEnum(PayloadScope), default=PayloadScope.User.value)\n    removal_date = Column(DateTime)\n    duration_until_removal = Column(BigInteger)\n    consent_en = Column(Text)\n    is_encrypted = Column(Boolean, default=False)\n\n    payloads = relationship('Payload',\n                            secondary=profile_payloads,\n                            backref='profiles')"
  },
  {
    "path": "commandment/deprecated/schema.py",
    "content": "\n# @register_payload_schema('com.apple.ADCertificate.managed')\n# class ADCertificatePayload(Payload):\n#     Description = fields.Str(attribute='description')\n#     CertServer = fields.Str(attribute='cert_server')\n#     CertTemplate = fields.Str(attribute='cert_template')\n#     CertificateAuthority = fields.Str(attribute='certificate_authority')\n#     CertificateAcquisitionMechanism = EnumField(ADCertificateAcquisitionMechanism, attribute='acquisition_mechanism')\n#     CertificateRenewalTimeInterval = fields.Int(attribute='renewal_time_interval')\n#     Keysize = fields.Int(attribute='keysize')\n#     UserName = fields.Str(attribute='username')\n#     Password = fields.Str(attribute='password')\n#     PromptForCredentials = fields.Bool(attribute='prompt_for_credentials')\n#     AllowAllAppsAccess = fields.Bool(attribute='allow_all_apps_access')\n#     KeyIsExtractable = fields.Bool(attribute='key_is_extractable')\n#\n#     @post_load\n#     def make_payload(self, data) -> models.ADCertPayload:\n#         return models.ADCertPayload(**data)\n\n\nclass QoSMarkingPolicy(Schema):\n    # QoSMarkingWhitelistedAppIdentifiers = fields.Array\n    QoSMarkingAppleAudioVideoCalls = fields.Boolean()\n    QoSMarkingEnabled = fields.Boolean()\n\n\nclass EAPClientConfiguration(Schema):\n    \"\"\"EAPOLClient configuration properties.\n\n    I have added several more unpublished properties from the EAP8012X source available via opensource.apple.com.\n    \"\"\"\n\n    UserName = fields.String()\n    UserPassword = fields.String()\n    UserPasswordKeychainItemID = fields.String()  # Unconfirmed\n    OneTimeUserPassword = fields.Boolean()  # Unconfirmed\n    OneTimePassword = fields.Boolean()\n    # AcceptEAPTypes = fields.Integer()\n    # InnerAcceptEAPTypes  # Unconfirmed\n    # PayloadCertificateAnchorUUID = fields.UUID()\n    # TLSTrustedServerNames\n    TLSAllowTrustExceptions = fields.Boolean()\n    TLSCertificateIsRequired = fields.Boolean()\n    \"\"\"- TLS-based authentication protocol requires a certificate to authenticate\n       - the default value is TRUE for EAP-TLS, FALSE otherwise\n       - allows for two-factor authentication (certificate + name/password)  \n         when set to TRUE for EAP-TTLS, PEAP, EAP-FAST\n       - allows for zero-factor authentication when set to FALSE for EAP-TLS\"\"\"\n    # TLSTrustedCertificates array<data> Unconfirmed\n    # TLSSaveTrustExceptions\n    # TLSTrustExceptionsDomain\n    # exceptions domain values:\n    # WirelessSSID\n    # ProfileID\n    # NetworkInterfaceName\n\n    # TLSTrustExceptionsID\n    # SaveCredentialsOnSuccessfulAuthentication\n    # TLSVerifyServerCertificate\n    # TLSEnableSessionResumption\n    # TLSUserTrustProceedCertificateChain\n    # SystemModeUseOpenDirectoryCredentials\n    # SystemModeOpenDirectoryNodeName\n\n\n    NewPassword = fields.String()\n    OuterIdentity = fields.String()\n    \"\"\"OuterIdentity: Applies to TTLS, PEAP, EAP-FAST.\"\"\"\n\n    TLSIdentityHandle = fields.String()\n    \"\"\"TLSIdentityHandle: TLS only\"\"\"\n\n\n\n    SystemModeCredentialsSource = fields.String()\n    TTLSInnerAuthentication = EnumField(TTLSInnerAuthentication)\n\n    # EAP-FAST\n    EAPFASTUsePAC = fields.Boolean()\n    EAPFASTProvisionPAC = fields.Boolean()\n    EAPFASTProvisionPACAnonymously = fields.Boolean()\n    EAPSIMNumberOfRANDs = fields.Integer()\n\n    # InnerEAPType\n    # InnerEAPTypeName\n    # TLSServerCertificateChain\n\n    # To Check: In EAP8012X source\n    # SystemModeUseOpenDirectoryCredentials\n    # SystemModeOpenDirectoryNodeName\n    #\n\n\n#\n# @register_payload_schema('com.apple.wifi.managed')\n# class WIFIPayload(Payload):\n#     SSID_STR = fields.Str(attribute='ssid_str')\n#     HIDDEN_NETWORK = fields.Boolean(attribute='hidden_network')\n#     AutoJoin = fields.Boolean(attribute='auto_join', allow_none=True)\n#     EncryptionType = EnumField(WIFIEncryptionType, attribute='encryption_type')\n#     IsHotspot = fields.Boolean(attribute='is_hotspot', allow_none=True)\n#     DomainName = fields.String(attribute='domain_name', allow_none=True)\n#     ServiceProviderRoamingEnabled = fields.Boolean(attribute='service_provider_roaming_enabled', allow_none=True)\n#     # RoamingConsortiumOIs = fields.Nested(fields.String(), many=True)\n#     # NAIRealmNames\n#     # MCCAndMNCs\n#     DisplayedOperatorName = fields.String(attribute='displayed_operator_name', allow_none=True)\n#     ProxyType = fields.String(attribute='proxy_type', allow_none=True)\n#     CaptiveBypass = fields.Boolean(attribute='captive_bypass', allow_none=True)\n#     QoSMarkingPolicy = fields.Nested(QoSMarkingPolicy(), allow_none=True)\n#\n#     Password = fields.String(attribute='password', allow_none=True)\n#     PayloadCertificateUUID = fields.UUID(attribute='payload_certificate_uuid', allow_none=True)\n#     EAPClientConfiguration = fields.Nested(EAPClientConfiguration(), allow_none=True)\n#\n#     @post_load\n#     def make_payload(self, data: dict) -> models.WIFIPayload:\n#         payload = models.WIFIPayload(**data)\n#         return payload\n\n\nclass EnergySaverSettings(Schema):\n    AutomaticRestartOnPowerLoss = fields.Integer(load_from='Automatic Restart On Power Loss')  # Pseudo boolean 0/1\n    DiskSleepTimerBoolean = fields.Boolean(load_from='Disk Sleep Timer-boolean')\n    DiskSleepTimer = fields.Integer(load_from='Display Sleep Timer')\n    SystemSleepTimer = fields.Integer(load_from='System Sleep Timer')\n    WakeOnLAN = fields.Integer(load_from='Wake On LAN')  # Pseudo boolean 0/1\n\n\nclass EnergySaverPowerSchedule(Schema):\n    eventtype = EnumField(ScheduledPowerEventType)\n    time = fields.Integer(validate=lambda n: 0 <= n <= 2400)\n    weekdays = fields.Integer()\n\n\nclass EnergySaverSchedules(Schema):\n    RepeatingPowerOn = fields.Nested(EnergySaverPowerSchedule)\n    RepeatingPowerOff = fields.Nested(EnergySaverPowerSchedule)\n\n\n@register_payload_schema('com.apple.MCX')\nclass EnergySaverPayload(Payload):\n    DestroyFVKeyOnStandby = fields.Boolean(attribute='destroy_fv_key_on_standby')\n    SleepDisabled = fields.Boolean(attribute='sleep_disabled')\n    DesktopACPowerProfileNumber = fields.Integer(load_from='com.apple.EnergySaver.desktop.ACPower-ProfileNumber')\n    PortableACPowerProfileNumber = fields.Integer(load_from='com.apple.EnergySaver.portable.ACPower-ProfileNumber')\n    PortableBatteryProfileNumber = fields.Integer(load_from='com.apple.EnergySaver.portable.BatteryPower-ProfileNumber')\n    DesktopACPower = fields.Nested(EnergySaverSettings, load_from='com.apple.EnergySaver.desktop.ACPower')\n    PortableACPower = fields.Nested(EnergySaverSettings, load_from='com.apple.EnergySaver.portable.ACPower')\n    PortableBatteryPower = fields.Nested(EnergySaverSettings, load_from='com.apple.EnergySaver.portable.BatteryPower')\n    Schedule = fields.Nested(EnergySaverSchedules, load_from='com.apple.EnergySaver.desktop.Schedule')\n"
  },
  {
    "path": "commandment/enroll/__init__.py",
    "content": "from enum import Enum\n\n\nclass DeviceAttributes(Enum):\n    \"\"\"This enumeration describes all of the device attributes available to OTA profile enrolment.\n    \"\"\"\n    UDID = 'UDID'\n    VERSION = 'VERSION'\n    PRODUCT = 'PRODUCT'\n    DEVICE_NAME = 'DEVICE_NAME'\n    SERIAL = 'SERIAL'\n    MODEL = 'MODEL'\n    MAC_ADDRESS_EN0 = 'MAC_ADDRESS_EN0'\n    MEID = 'MEID'\n    IMEI = 'IMEI'\n    ICCID = 'ICCID'\n    COMPROMISED = 'COMPROMISED'\n    DeviceID = 'DeviceID'\n#    SPIROM = 'SPIROM'\n#    MLB = 'MLB'\n\n\nAllDeviceAttributes = {\n    DeviceAttributes.UDID.value,\n    DeviceAttributes.VERSION.value,\n    DeviceAttributes.PRODUCT.value,\n    DeviceAttributes.DEVICE_NAME.value,\n    DeviceAttributes.SERIAL.value,\n    DeviceAttributes.MODEL.value,\n    # DeviceAttributes.MAC_ADDRESS_EN0.value,\n    DeviceAttributes.MEID.value,\n    DeviceAttributes.IMEI.value,\n    DeviceAttributes.ICCID.value,\n    DeviceAttributes.COMPROMISED.value,\n    DeviceAttributes.DeviceID.value,\n#    DeviceAttributes.SPIROM.value,\n#    DeviceAttributes.MLB.value,\n}\n\n"
  },
  {
    "path": "commandment/enroll/app.py",
    "content": "\"\"\"\nThe enroll blueprint covers all enrolment scenarios such as:\n\n- Over-the-Air profile delivery\n- Direct enrolment (delivering a com.apple.mdm payload)\n\n\"\"\"\n\nfrom uuid import uuid4\nimport plistlib\n\nfrom flask import current_app, render_template, abort, Blueprint, make_response, url_for, request, g\nimport os\n\nfrom commandment.enroll import AllDeviceAttributes\nfrom commandment.enroll.profiles import ca_trust_payload_from_configuration, scep_payload_from_configuration, \\\n    identity_payload\nfrom commandment.profiles.models import MDMPayload, Profile, PEMCertificatePayload, DERCertificatePayload, SCEPPayload\nfrom commandment.profiles import PROFILE_CONTENT_TYPE, plist_schema as profile_schema, PayloadScope\nfrom commandment.models import db, Organization, SCEPConfig\nfrom sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound\nfrom commandment.plistutil.nonewriter import dumps as dumps_none\nfrom commandment.enroll.util import generate_enroll_profile\nfrom commandment.cms.decorators import verify_cms_signers\nfrom commandment.pki.ca import get_ca\n\nenroll_app = Blueprint('enroll_app', __name__)\n\n\ndef base64_to_pem(crypto_type, b64_text, width=76):\n    lines = ''\n    for pos in range(0, len(b64_text), width):\n        lines += b64_text[pos:pos + width] + '\\n'\n\n    return '-----BEGIN %s-----\\n%s-----END %s-----' % (crypto_type, lines, crypto_type)\n\n\n@enroll_app.route('/trust.mobileconfig', methods=['GET'])\ndef trust_mobileconfig():\n    \"\"\"Generate a trust profile, if one is required.\n\n    :resheader Content-Type: application/x-apple-aspen-config\n    :statuscode 200:\n    :statuscode 500: The system has not been configured, so we can't produce anything.\n    \"\"\"\n    try:\n        org = db.session.query(Organization).one()\n    except NoResultFound:\n        abort(500, 'No organization is configured, cannot generate enrollment profile.')\n    except MultipleResultsFound:\n        abort(500, 'Multiple organizations, backup your database and start again')\n\n    profile = Profile(\n        identifier=org.payload_prefix + '.trust',\n        uuid=uuid4(),\n        display_name='Commandment Trust Profile',\n        description='Allows your device to trust the MDM server',\n        organization=org.name,\n        version=1,\n        scope=PayloadScope.System,\n    )\n\n    if 'CA_CERTIFICATE' in current_app.config:\n        # If you specified a CA certificate, we assume it isn't a CA trusted by Apple devices.\n        ca_payload = ca_trust_payload_from_configuration()\n        profile.payloads.append(ca_payload)\n\n    if 'SSL_CERTIFICATE' in current_app.config:\n        basepath = os.path.dirname(__file__)\n        certpath = os.path.join(basepath, current_app.config['SSL_CERTIFICATE'])\n        with open(certpath, 'rb') as fd:\n            pem_payload = PEMCertificatePayload(\n                uuid=uuid4(),\n                identifier=org.payload_prefix + '.ssl',\n                payload_content=fd.read(),\n                display_name='Web Server Certificate',\n                description='Required for your device to trust the server',\n                type='com.apple.security.pkcs1',\n                version=1\n            )\n            profile.payloads.append(pem_payload)\n\n    schema = profile_schema.ProfileSchema()\n    result = schema.dump(profile)\n    plist_data = dumps_none(result.data, skipkeys=True)\n\n    return plist_data, 200, {'Content-Type': PROFILE_CONTENT_TYPE,\n                             'Content-Disposition': 'attachment; filename=\"trust.mobileconfig\"'}\n\n\n@enroll_app.route('/profile', methods=['GET', 'POST'])\ndef enroll():\n    \"\"\"Generate an enrollment profile.\"\"\"\n\n    ca = get_ca()\n    key, csr = ca.create_device_csr('device-identity')\n    device_certificate = ca.sign(csr)\n\n    pkcs12_payload = identity_payload(key, device_certificate, 'sekret')\n    profile = generate_enroll_profile(pkcs12_payload)\n\n    schema = profile_schema.ProfileSchema()\n    result = schema.dump(profile)\n    plist_data = dumps_none(result.data, skipkeys=True)\n\n    return plist_data, 200, {'Content-Type': PROFILE_CONTENT_TYPE}\n\n\n@enroll_app.route('/ota')\ndef ota_enroll():\n    \"\"\"Over-The-Air Profile Delivery Phase 1.5.\n\n    This endpoint represents the delivery of the `Profile Service` profile that should be delivered AFTER the user has\n    successfully authenticated.\n    \"\"\"\n    try:\n        org = db.session.query(Organization).one()\n    except NoResultFound:\n        abort(500, 'No organization is configured, cannot generate enrollment profile.')\n    except MultipleResultsFound:\n        abort(500, 'Multiple organizations, backup your database and start again')\n\n    profile = {\n        'PayloadType': 'Profile Service',\n        'PayloadIdentifier': org.payload_prefix + '.ota.enroll',\n        'PayloadUUID': 'FACC45E7-CB0E-4F8B-AA3E-E22DC161E25E', #str(uuid4()),\n        'PayloadVersion': 1,\n        'PayloadDisplayName': 'Commandment Profile Service',\n        'PayloadDescription': 'Enrolls your device with Commandment',\n        'PayloadOrganization': org.name,\n        'PayloadContent': {\n            'URL': 'https://{}:{}/enroll/ota_authenticate'.format(\n                current_app.config['PUBLIC_HOSTNAME'], current_app.config['PORT']\n            ),\n            'DeviceAttributes': list(AllDeviceAttributes),\n            'Challenge': 'TODO',\n        },\n    }\n    plist_data = dumps_none(profile)\n\n    return plist_data, 200, {'Content-Type': PROFILE_CONTENT_TYPE}\n\n\n@enroll_app.route('/ota_authenticate', methods=['POST'])\n@verify_cms_signers\ndef ota_authenticate():\n    \"\"\"Over-The-Air Profile Delivery Phase 3 and 4.\n\n    This endpoint represents the OTA Phase 3 and 4, \"/profile\" endpoint as specified in apples document \"Over-The-Air\n    Profile Delivery\".\n\n    There are two types of requests made here:\n    - The first request is signed by the iPhone Device CA and contains the challenge in the `Profile Service` payload,\n        we respond with the SCEP detail.\n    - The second request is signed by the issued SCEP certificate. We should respond with an enrollment profile.\n        It also contains the same device attributes sent in the previous step, but this time they are authenticated by\n        our SCEP CA.\n\n    Examples:\n\n    Signed plist given in the first request::\n\n        {\n            'CHALLENGE': '<CHALLENGE FROM PROFILE HERE>',\n            'IMEI': 'empty if macOS',\n            'MEID': 'empty if macOS',\n            'NotOnConsole': False,\n            'PRODUCT': 'MacPro6,1',\n            'SERIAL': 'C020000000000',\n            'UDID': '00000000-0000-0000-0000-000000000000',\n            'UserID': '00000000-0000-0000-0000-000000000000',\n            'UserLongName': 'Joe User',\n            'UserShortName': 'juser',\n            'VERSION': '16F73'\n        }\n\n    See Also:\n        - `Over-the-Air Profile Delivery and Configuration <https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/iPhoneOTAConfiguration/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009505-CH1-SW1>`_.\n    \"\"\"\n    signed_data = g.signed_data\n    # TODO: This should Validate to iPhone Device CA but we can't because:\n    # http://www.openradar.me/31423312\n    device_attributes = plistlib.loads(signed_data)\n\n    current_app.logger.debug(device_attributes)\n\n    try:\n        org = db.session.query(Organization).one()\n    except NoResultFound:\n        abort(500, 'No organization is configured, cannot generate enrollment profile.')\n    except MultipleResultsFound:\n        abort(500, 'Multiple organizations, backup your database and start again')\n\n    # TODO: Behold, the stupidest thing ever just to get this working, theres no way this should be prod:\n    # Phase 4 does not send a challenge but phase 3 does\n    if 'CHALLENGE' in device_attributes:\n        # Reply SCEP\n        profile = Profile(\n            identifier=org.payload_prefix + '.ota.phase3',\n            uuid=uuid4(),\n            display_name='Commandment OTA SCEP Enrollment',\n            description='Retrieves a SCEP Certificate to complete OTA Enrollment',\n            organization=org.name,\n            version=1,\n            scope=PayloadScope.System,\n        )\n\n        scep_payload = scep_payload_from_configuration()\n        profile.payloads.append(scep_payload)\n    else:\n        profile = generate_enroll_profile()\n\n    schema = profile_schema.ProfileSchema()\n    result = schema.dump(profile)\n    plist_data = dumps_none(result.data, skipkeys=True)\n\n    return plist_data, 200, {'Content-Type': PROFILE_CONTENT_TYPE}\n\n"
  },
  {
    "path": "commandment/enroll/profiles.py",
    "content": "import os.path\nfrom typing import Optional\nfrom uuid import uuid4\nfrom flask import abort, current_app, url_for\nfrom sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound\n\nfrom commandment.profiles.certificates import KeyUsage\nfrom commandment.profiles.models import SCEPPayload, PEMCertificatePayload, PKCS12CertificatePayload\nfrom commandment.models import db, Organization, SCEPConfig\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography import x509\nfrom commandment.pki.openssl import create_pkcs12\n\n\ndef scep_payload_from_configuration() -> SCEPPayload:\n    \"\"\"Generate a SCEP Payload based upon the commandment system configuration.\n\n    Returns:\n        SCEPPayload: The created payload based upon current configuration.\n    \"\"\"\n    # try:\n    #     org = db.session.query(Organization).one()\n    # except NoResultFound:\n    #     abort(500, 'No organization is configured, cannot generate enrollment profile.')\n    # except MultipleResultsFound:\n    #     abort(500, 'Multiple organizations, backup your database and start again')\n\n    try:\n        scep_config = db.session.query(SCEPConfig).one()\n\n        scep_payload = SCEPPayload(\n            uuid=uuid4(),\n            identifier='com.github.cmdmnt.commandment.scep',\n            url=scep_config.url,\n            name='',\n            subject=[['CN', '%HardwareUUID%']],\n            challenge=scep_config.challenge,\n            key_size=scep_config.key_size,\n            key_type='RSA',\n            key_usage=scep_config.key_usage,\n            display_name='Commandment SCEP Enroll Payload',\n            description='Requests a certificate to identify your device to commandment',\n            retries=scep_config.retries,\n            retry_delay=scep_config.retry_delay,\n            version=1\n        )\n    except NoResultFound:\n        scep_payload = SCEPPayload(\n            uuid=uuid4(),\n            identifier='com.github.cmdmnt.commandment.scep',\n            url=url_for('scep_app.scep', _external=True),\n            name='COMMANDMENT-SCEP',\n            subject=[['CN', '%HardwareUUID%']],\n            challenge=current_app.config.get('SCEPY_CHALLENGE', None),\n            key_size=2048,\n            key_type='RSA',\n            key_usage=KeyUsage.All,\n            display_name='Commandment SCEP Enroll Payload',\n            description='Requests a certificate to identify your device to commandment',\n            retries=3,\n            retry_delay=10,\n            version=1\n        )\n    except MultipleResultsFound:\n        return abort(500, 'Multiple SCEP configs, this should never happen.')\n\n    return scep_payload\n\n\ndef ca_trust_payload_from_configuration() -> PEMCertificatePayload:\n    \"\"\"Create a CA payload with the PEM representation of the Certificate Authority used by this instance.\n\n    You need to check whether the app config contains 'CA_CERTIFICATE' before invoking this.\n    \"\"\"\n    try:\n        org = db.session.query(Organization).one()\n    except NoResultFound:\n        abort(500, 'No organization is configured, cannot generate enrollment profile.')\n    except MultipleResultsFound:\n        abort(500, 'Multiple organizations, backup your database and start again')\n\n    with open(current_app.config['CA_CERTIFICATE'], 'rb') as fd:\n        pem_data = fd.read()\n        pem_payload = PEMCertificatePayload(\n            uuid=uuid4(),\n            identifier=org.payload_prefix + '.ca',\n            payload_content=pem_data,\n            display_name='Certificate Authority',\n            description='Required for your device to trust the server',\n            type='com.apple.security.root',\n            version=1\n        )\n\n        return pem_payload\n\n\ndef ssl_trust_payload_from_configuration() -> PEMCertificatePayload:\n    \"\"\"Generate a PEM certificate payload in order to trust this host.\n\n    \"\"\"\n    try:\n        org = db.session.query(Organization).one()\n    except NoResultFound:\n        abort(500, 'No organization is configured, cannot generate enrollment profile.')\n    except MultipleResultsFound:\n        abort(500, 'Multiple organizations, backup your database and start again')\n\n    basepath = os.path.dirname(__file__)\n    certpath = os.path.join(basepath, current_app.config['SSL_CERTIFICATE'])\n\n    with open(certpath, 'rb') as fd:\n        pem_payload = PEMCertificatePayload(\n            uuid=uuid4(),\n            identifier=org.payload_prefix + '.ssl',\n            payload_content=fd.read(),\n            display_name='Web Server Certificate',\n            description='Required for your device to trust the server',\n            type='com.apple.security.pkcs1',\n            version=1\n        )\n        return pem_payload\n\n\ndef identity_payload(private_key: rsa.RSAPrivateKeyWithSerialization,\n                     certificate: x509.Certificate,\n                     passphrase: Optional[str] = None) -> PKCS12CertificatePayload:\n    \"\"\"Generate a PKCS#12 certificate payload for device identity.\"\"\"\n    try:\n        org = db.session.query(Organization).one()\n    except NoResultFound:\n        abort(500, 'No organization is configured, cannot generate enrollment profile.')\n    except MultipleResultsFound:\n        abort(500, 'Multiple organizations, backup your database and start again')\n\n    pkcs12_data = create_pkcs12(private_key, certificate, passphrase)\n\n    pkcs12_payload = PKCS12CertificatePayload(\n        uuid=uuid4(),\n        certificate_file_name='device_identity.p12',\n        identifier=org.payload_prefix + '.identity',\n        display_name='Device Identity Certificate',\n        description='Required to identify your device to the MDM',\n        type='com.apple.security.pkcs12',\n        password=passphrase,\n        payload_content=pkcs12_data,\n        version=1\n    )\n\n    return pkcs12_payload\n"
  },
  {
    "path": "commandment/enroll/util.py",
    "content": "import os.path\nfrom typing import Optional\nfrom flask import abort, current_app\n\nfrom commandment.enroll.profiles import scep_payload_from_configuration, ca_trust_payload_from_configuration, \\\n    ssl_trust_payload_from_configuration\nfrom commandment.models import db, Organization\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.x509.name import NameOID\nfrom sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound\nfrom commandment.profiles import PayloadScope\nfrom commandment.profiles.models import Profile, MDMPayload, PKCS12CertificatePayload\nfrom uuid import uuid4\n\n\ndef generate_enroll_profile(pkcs12_payload: Optional[PKCS12CertificatePayload] = None) -> Profile:\n    \"\"\"Generate an enrollment profile.\n\n    If the user specified a CA certificate, we assume that it won't be trusted by default, so it is included in the\n    enrollment profile.\n\n    If the user specified an SSL certificate, we assume that it won't be trusted by default.\n\n    You need to have an organization configured to generate organization information in the profile, and to establish\n    the payload prefix.\n\n    The enrollment profile reserves the use of UUID: 1355300-1111-1111-1111-868EC47093C3\n\n    Args:\n        pkcs12_payload (Optional[PKCS12CertificatePayload): A PKCS#12 Payload if we are supplying device identity without\n            using SCEP\n\n    \"\"\"\n    try:\n        org = db.session.query(Organization).one()\n    except NoResultFound:\n        abort(500, 'No organization is configured, cannot generate enrollment profile.')\n    except MultipleResultsFound:\n        abort(500, 'Multiple organizations, backup your database and start again')\n\n    push_certificate_path = os.path.join(os.path.dirname(current_app.root_path), current_app.config['PUSH_CERTIFICATE'])\n\n    if os.path.exists(push_certificate_path):\n        push_certificate_basename, ext = os.path.splitext(push_certificate_path)\n        if ext.lower() == '.p12':  # push service will have re-exported the PKCS#12 container\n            push_certificate_path = push_certificate_basename + '.crt'\n\n        with open(push_certificate_path, 'rb') as fd:\n            push_certificate = x509.load_pem_x509_certificate(fd.read(), backend=default_backend())\n    else:\n        abort(500, 'No push certificate available at: {}'.format(push_certificate_path))\n\n    if not org.payload_prefix:\n        abort(500, 'MDM configuration has no profile prefix')\n\n    profile = Profile(\n        identifier=org.payload_prefix + '.enroll',\n        uuid=uuid4(),\n        display_name='Commandment Enrollment Profile',\n        description='Enrolls your device for Mobile Device Management',\n        organization=org.name,\n        version=1,\n        scope=PayloadScope.System,\n    )\n\n    if 'CA_CERTIFICATE' in current_app.config:\n        # If you specified a CA certificate, we assume it isn't a CA trusted by Apple devices.\n        ca_payload = ca_trust_payload_from_configuration()\n        profile.payloads.append(ca_payload)\n\n    # Include Self Signed Certificate if necessary\n    # TODO: Check that cert is self signed.\n    if 'SSL_CERTIFICATE' in current_app.config:\n        ssl_payload = ssl_trust_payload_from_configuration()\n        profile.payloads.append(ssl_payload)\n\n    if pkcs12_payload is None:\n        scep_payload = scep_payload_from_configuration()\n        profile.payloads.append(scep_payload)\n        cert_uuid = scep_payload.uuid\n    else:\n        profile.payloads.append(pkcs12_payload)\n        cert_uuid = pkcs12_payload.uuid\n\n    from commandment.mdm import AccessRights\n\n    push_topics = push_certificate.subject.get_attributes_for_oid(NameOID.USER_ID)\n    if len(push_topics) != 1:\n        abort(500, 'Unexpected missing or invalid push topic in Push Certificate')\n\n    push_topic = push_topics[0].value\n\n    mdm_payload = MDMPayload(\n        uuid=uuid4(),\n        identifier=org.payload_prefix + '.mdm',\n        identity_certificate_uuid=cert_uuid,\n        topic=push_topic,\n        server_url='https://{}:{}/mdm'.format(current_app.config['PUBLIC_HOSTNAME'], current_app.config['PORT']),\n        access_rights=AccessRights.All.value,\n        check_in_url='https://{}:{}/checkin'.format(current_app.config['PUBLIC_HOSTNAME'], current_app.config['PORT']),\n        sign_message=True,\n        check_out_when_removed=True,\n        display_name='Device Configuration and Management',\n        server_capabilities=['com.apple.mdm.per-user-connections'],\n        description='Enrolls your device with the MDM server',\n        version=1\n    )\n    profile.payloads.append(mdm_payload)\n\n    return profile\n"
  },
  {
    "path": "commandment/errors.py",
    "content": "from typing import Optional, Dict\nfrom flask import jsonify\n\n\nclass JSONAPIError(Exception):\n\n    def __init__(self, title: str, status: int = 500, code: Optional[str] = None, detail: Optional[str] = None,\n                 source: Optional[Dict[str, str]] = None, meta=None, id=None):\n        self.title = title\n        self.status = status\n        self.code = code\n        self.detail = detail\n        self.source = source\n        self.meta = meta\n        self.id = id\n\n    def to_dict(self) -> Dict[str, any]:\n        res = {'errors': []}\n        error = {'title': self.title, 'status': self.status}\n        if self.code is not None:\n            error['code'] = self.code\n\n        if self.detail is not None:\n            error['detail'] = self.detail\n\n        res['errors'].append(error)\n        return res\n"
  },
  {
    "path": "commandment/inventory/__init__.py",
    "content": "\n# These applications should not be reported on because they are part of the operating system.\nDEFAULT_BUNDLE_ID_BLACKLIST = [\n    'com.apple.PubSubAgent',\n    'com.apple.IMServicePlugInAgent',\n    'com.apple.print.PrinterProxy',\n    'com.apple.speech.synthesis.SpeechSynthesisServer',\n    'com.apple.FontRegistryUIAgent',\n    'com.apple.AddressBook.sync',\n    'com.apple.AddressBookSourceSync',\n    'com.apple.AddressBook.abd',\n    'com.apple.ABAssistantService',\n    'com.apple.check_afp',\n    'com.apple.screencapturetb',\n    'com.apple.rcd',\n    'com.apple.loginwindow',\n    'com.apple.CloudKit.ShareBear',\n    'com.apple.cloudphotosd',\n    'com.apple.ZoomWindow.app',\n    'com.apple.wifi.WiFiAgent',\n    'com.apple.weather',\n    'com.apple.UserNotificationCenter',\n    'com.apple.VoiceOver',\n    'com.apple.UnmountAssistantAgent',\n    'com.apple.UniversalAccessControl',\n    'com.apple.Ticket-Viewer',\n    'com.apple.ThermalTrap',\n    'com.apple.systemuiserver',\n    'com.apple.systemevents',\n    'com.apple.stocks',\n    'com.apple.SoftwareUpdate',\n    'com.apple.SocialPushAgent',\n    'com.apple.SecurityFixer',\n    'com.apple.ScriptMonitor',\n    'com.apple.ReportPanic',\n    'com.apple.RemoteDesktopAgent',\n    'com.apple.pluginIM.pluginIMRegistrator',\n    'com.apple.RapportUIAgent',\n    'com.apple.ProblemReporter',\n    'com.apple.PowerChime',\n    'com.apple.Pass-Viewer',\n    'com.apple.PIPAgent',\n    'com.apple.OSDUIHelper',\n    'com.apple.ODSAgent',\n    'com.apple.OBEXAgent',\n    'com.apple.notificationcenterui',\n    'com.apple.NowPlayingTouchUI',\n    'com.apple.NetworkDiagnostics',\n    'com.apple.NetAuthAgent',\n    'com.apple.MemorySlotUtility',\n    'com.apple.MRT',\n    'com.apple.locationmenu',\n    'com.apple.Language-Chooser',\n    'com.apple.security.Keychain-Circle-Notification',\n    'com.apple.KeyboardSetupAssistant',\n    'com.apple.JavaWebStart',\n    'com.apple.JarLauncher',\n    'com.apple.installer',\n    'com.apple.Installer-Progress',\n    'com.apple.PackageKit.Install-in-Progress',\n    'com.apple.dt.CommandLineTools.installondemand',\n    'com.apple.imageevents',\n    'com.apple.helpviewer',\n    'com.apple.gamecenter',\n    'com.apple.FolderActionsDispatcher',\n    'com.apple.FirmwareUpdateHelper',\n    'com.apple.finder.Open-iCloudDrive',\n    'com.apple.finder.Open-Network',\n    'com.apple.finder.Open-Computer',\n    'com.apple.finder.Open-AllMyFiles',\n    'com.apple.finder.Open-AirDrop',\n    'com.apple.ExpansionSlotUtility',\n    'com.apple.EscrowSecurityAlert',\n    'com.apple.DwellControl',\n    'com.apple.dock',\n    'com.apple.DiskImageMounter',\n    'com.apple.DiscHelper',\n    'com.apple.databaseevents',\n    'com.apple.coreservices.uiagent',\n    'com.apple.CoreLocationAgent',\n    'com.apple.controlstrip',\n    'com.apple.CaptiveNetworkAssistant',\n    'com.apple.CalendarFileHandler',\n    'com.apple.BluetoothUIServer',\n    'com.apple.BluetoothSetupAssistant',\n    'com.apple.AutomatorRunner',\n    'com.apple.wifi.diagnostics',\n    'com.apple.SystemImageUtility',\n    'com.apple.StorageManagementLauncher',\n    'com.apple.ScreenSharing',\n    'com.apple.RAIDUtility',\n    'com.apple.NetworkUtility',\n    'com.apple.FolderActionsSetup',\n    'com.apple.appleseed.FeedbackAssistant',\n    'com.apple.archiveutility',\n    'com.apple.AboutThisMacLauncher',\n    'com.apple.AppleScriptUtility',\n    'com.apple.AppleGraphicsWarning',\n    'com.apple.AppleFileServer',\n    'com.apple.appstore.AppDownloadLauncher',\n    'com.apple.AirPortBaseStationAgent',\n    'com.apple.AirPlayUIAgent',\n    'com.apple.AddressBook.UrlForwarder',\n    'com.apple.AVB-Audio-Configuration',\n    'com.apple.ColorSyncCalibrator',\n    'com.apple.SyncServices.AppleMobileSync',\n    'com.apple.SyncServices.AppleMobileDeviceHelper',\n    'com.apple.WebKit.PluginHost',\n    'com.apple.WebProcess',\n    'com.apple.WebKit.PluginProcess',\n    'com.apple.WebKit.NetworkProcess',\n    'com.apple.WebKit.DatabaseProcess',\n    'com.apple.mrt.uiagent',\n    'com.apple.WebKit.PluginHost',\n    'com.apple.syncserver',\n    'com.apple.ScreenSaver.Engine',\n    'com.apple.QuickLookDaemon32',\n    'com.apple.VoiceOverQuickstart',\n    'com.apple.CharacterPaletteIM',\n    'com.apple.DirectoryUtility',\n    'com.apple.SetupAssistant',\n    'com.apple.PhotoLibraryMigrationUtility',\n    'com.apple.NetworkSetupAssistant',\n    'com.apple.ManagedClient',\n    'com.apple.finder',\n    'com.apple.CertificateAssistant',\n    'com.apple.print.add',\n    'com.adobe.dynamiclinkmediaserver',\n    'com.apple.Family',\n    'com.apple.familycontrols.useragent',\n    'com.apple.frameworks.diskimages.diuiagent',\n    'com.apple.FollowUpUI',\n    'com.apple.CCE.CIMFindInputCode',\n    'com.apple.cmfsyncagent',\n    'com.apple.storeuid',\n    'com.apple.lateragent',\n    'com.apple.bird',\n    'com.apple.Calibration-Assistant',\n    'com.apple.AOSPushRelay',\n    'com.apple.AOSHeartbeat',\n    'com.apple.AOSAlertManager',\n    'com.apple.iCloudUserNotificationsd',\n    'com.apple.TrackpadIM-Container',\n    'com.apple.VIM-Container',\n    'com.apple.inputmethod.Tamil',\n    'com.apple.TCIM-Container',\n    'com.apple.inputmethod.AssistiveControl',\n    'com.apple.SCIM-Container',\n    'com.apple.PAH-Container',\n    'com.apple.inputmethod.PluginIM',\n    'com.apple.KIM-Container',\n    'com.apple.KeyboardViewer',\n    'com.apple.JapaneseIM-Container',\n    'com.apple.ink.inkserver',\n    'com.apple.HIM-Container',\n    'com.apple.inputmethod.EmojiFunctionRowItem-Container',\n    'com.apple.inputmethod.ironwood',\n    'com.apple.inputmethod.Ainu',\n    'com.apple.50onPaletteIM',\n    'com.apple.AutoImporter',\n    'com.apple.VirtualScanner',\n    'com.apple.Type8Camera',\n    'com.apple.Type5Camera',\n    'com.apple.Type4Camera',\n    'com.apple.PTPCamera',\n    'com.apple.MassStorageCamera',\n    'com.apple.AirScanScanner',\n    'com.apple.BuildWebPage',\n    'com.apple.WebKit.PluginHost',\n    'com.apple.cmfsyncagent',\n    'com.apple.imavagent',\n    'com.apple.idsfoundation.IDSRemoteURLConnectionAgent',\n    'com.apple.ids.IDSCredentialsAgent',\n    'com.apple.identityservicesd',\n    'com.apple.imautomatichistorydeletionagent',\n    'com.apple.imagent',\n    'com.apple.imtransferservices.IMTransferAgent',\n    'com.apple.ImageCaptureService',\n    'com.apple.syncservices.syncuid',\n    'com.apple.speech.SpeechDataInstallerd',\n    'com.apple.eap8021x.eaptlstrust',\n    'com.apple.AskPermissionUI',\n    'com.apple.MakePDF',\n    'com.apple.QuickLookDaemon',\n    'com.apple.quicklook.ui.helper',\n    'com.apple.notificationcenter.widgetsimulator',\n    'com.apple.Spotlight',\n    'com.apple.Siri',\n    'com.apple.InstallAssistant.HighSierra',\n    'com.apple.ManagedClient',\n    'com.apple.InstallAssistant.HighSierra',\n    'com.apple.FindMyMacMessenger',\n    'com.apple.idsfoundation.IDSRemoteURLConnectionAgent',\n    'com.apple.identityservicesd',\n    'com.apple.imavagent',\n    'com.apple.imagent',\n    'com.apple.imautomatichistorydeletionagent',\n    'com.apple.imtransferservices.IMTransferAgent',\n    'com.apple.soagent',\n    'com.apple.SyncServices.AppleMobileSync',\n    'com.apple.SyncServices.AppleMobileDeviceHelper',\n    'com.apple.nbagent',\n    'com.apple.ScreenReaderUIServer',\n    'com.apple.speech.SpeechRecognitionServer',\n    'com.apple.STMFramework.UIHelper',\n    'com.apple.syncservices.ConflictResolver',\n    'com.apple.accessibility.universalAccessAuthWarn',\n    'com.apple.accessibility.universalAccessHUD',\n    'com.apple.accessibility.DFRHUD',\n    'com.apple.coreservices.UASharedPasteboardProgressUI',\n    'com.apple.ChineseTextConverterService',\n    'com.apple.SummaryService',\n]\n"
  },
  {
    "path": "commandment/inventory/api.py",
    "content": "from flask import Blueprint, send_file\nfrom flask_rest_jsonapi import Api\nimport io\n\nfrom commandment.inventory.models import db, InstalledCertificate\nfrom commandment.inventory.resources import InstalledApplicationsList, InstalledApplicationDetail, \\\n    InstalledCertificatesList, InstalledCertificateDetail, InstalledProfilesList, InstalledProfileDetail, \\\n    AvailableOSUpdateList, AvailableOSUpdateDetail\nfrom commandment.api.app_jsonapi import api\n\napi_app = Blueprint('inventory_api_app', __name__)\n# api = Api(blueprint=api_app)\n\n# InstalledApplications\napi.route(InstalledApplicationsList, 'installed_applications_list',\n          '/v1/installed_applications', '/v1/devices/<int:device_id>/installed_applications')\napi.route(InstalledApplicationDetail, 'installed_application_detail',\n          '/v1/installed_applications/<int:installed_application_id>')\n\n# InstalledCertificates\napi.route(InstalledCertificatesList, 'installed_certificates_list',\n          '/v1/installed_certificates', '/v1/devices/<int:device_id>/installed_certificates')\napi.route(InstalledCertificateDetail, 'installed_certificate_detail',\n          '/v1/installed_certificates/<int:installed_certificate_id>')\n\napi.route(InstalledProfilesList, 'installed_profiles_list', '/v1/installed_profiles',\n          '/v1/devices/<int:device_id>/installed_profiles')\napi.route(InstalledProfileDetail, 'installed_profile_detail', '/v1/installed_profiles/<int:installed_profile_id>')\n\n\n\n# Available OS Updates\napi.route(AvailableOSUpdateList, 'available_os_updates_list',\n          '/v1/available_os_updates', '/v1/devices/<int:device_id>/available_os_updates')\napi.route(AvailableOSUpdateDetail, 'available_os_update_detail',\n          '/v1/available_os_updates/<int:available_os_update_id>')\n\n\n@api_app.route('/v1/installed_certificates/<int:installed_certificate_id>/download')\ndef download_installed_certificate(installed_certificate_id: int):\n    \"\"\"Download an installed certificate asx a DER encoded X.509 certificate.\n\n    The file name will be a stripped version of the X.509 Common Name, with a .crt extension.\n\n    :reqheader Accept: application/x-x509-ca-cert\n    :resheader Content-Type: application/x-x509-ca-cert\n    :statuscode 200: OK\n    :statuscode 404: Not found\n    :statuscode 400: Can't produce requested encoding\n    \"\"\"\n    c = db.session.query(InstalledCertificate).filter(InstalledCertificate.id == installed_certificate_id).one()\n    bio = io.BytesIO(c.der_data)\n\n    prefix = c.x509_cn.strip('/\\:') if c.x509_cn is not None else 'certificate'\n\n    return send_file(bio, 'application/x-x509-ca-cert', True, '{}.crt'.format(prefix))\n"
  },
  {
    "path": "commandment/inventory/models.py",
    "content": "from sqlalchemy.ext.mutable import MutableList\n\nfrom commandment.models import db\nfrom commandment.dbtypes import GUID, JSONEncodedDict\n\n\nclass InstalledApplication(db.Model):\n    \"\"\"This model represents a single application that was returned as part of an ``InstalledApplicationList`` query.\n\n    It is impossible to create a composite key to uniquely identify each row, therefore every time the device reports\n    back we need to wipe all rows associated with a single device. The reason why a composite key won't work here is\n    that macOS will often report the binary name and no identifier, version, or size (and sometimes iOS can do the\n    inverse of that).\n\n    :table: installed_applications\n\n    See Also:\n          - `InstalledApplicationList Command <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html#//apple_ref/doc/uid/TP40017387-CH3-SW14>`_.\n    \"\"\"\n    __tablename__ = 'installed_applications'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (int): Identifier\"\"\"\n    device_udid = db.Column(db.String(40), index=True, nullable=False)\n    \"\"\"device_udid (GUID): Unique device identifier\"\"\"\n    device_id = db.Column(db.ForeignKey('devices.id'), nullable=True)\n    \"\"\"device_id (int): Parent relationship ID of the device\"\"\"\n    device = db.relationship('Device', backref='installed_applications')\n    \"\"\"device (db.relationship): SQLAlchemy relationship to the device.\"\"\"\n\n    # Many of these can be empty, so there is no valid composite key\n    bundle_identifier = db.Column(db.String, index=True)\n    \"\"\"bundle_identifier (str): The com.xxx.yyy bundle identifier for the application. May be empty.\"\"\"\n    version = db.Column(db.String, index=True)\n    \"\"\"version (str): The long version for the application. May be empty.\"\"\"\n    short_version = db.Column(db.String)\n    \"\"\"short_version (str): The short version for the application. May be empty.\"\"\"\n    name = db.Column(db.String)\n    \"\"\"name (str): The application name\"\"\"\n    bundle_size = db.Column(db.BigInteger)\n    \"\"\"bundle_size (int): The application size\"\"\"\n    dynamic_size = db.Column(db.BigInteger)\n    \"\"\"dynamic_size (int): The dynamic data size (for iOS containers).\"\"\"\n    is_validated = db.Column(db.Boolean)\n    \"\"\"is_validated (bool):\"\"\"\n    external_version_identifier = db.Column(db.BigInteger, index=True)\n    \"\"\"external_version_identifier (int): The application’s external version ID. \n       It can be used for comparison in the iTunes Search API to decide if the application needs to be updated.\"\"\"\n    adhoc_codesigned = db.Column(db.Boolean)\n    appstore_vendable = db.Column(db.Boolean)\n    beta_app = db.Column(db.Boolean)\n    device_based_vpp = db.Column(db.Boolean)\n    has_update_available = db.Column(db.Boolean)\n    installing = db.Column(db.Boolean)\n\n\nclass InstalledCertificate(db.Model):\n    \"\"\"This model represents a single installed certificate on an enrolled device as returned by the ``CertificateList``\n    query.\n\n    The response will usually include both certificates managed by profiles and certificates that were installed\n    outside of a profile.\n\n    :table: installed_certificates\n\n    See Also:\n          - `CertificateList Command <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html#//apple_ref/doc/uid/TP40017387-CH3-SW13>`_.\n    \"\"\"\n    __tablename__ = 'installed_certificates'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"(int): Installed Certificate ID\"\"\"\n    device_udid = db.Column(db.String(40), index=True, nullable=False)\n    \"\"\"(GUID): Unique Device Identifier\"\"\"\n    device_id = db.Column(db.ForeignKey('devices.id'), nullable=True)\n    \"\"\"(int): Device foreign key ID.\"\"\"\n    device = db.relationship('Device', backref='installed_certificates')\n    \"\"\"(db.relationship): Device relationship\"\"\"\n    x509_cn = db.Column(db.String)\n    \"\"\"(str): The X.509 Common Name of the certificate.\"\"\"\n    is_identity = db.Column(db.Boolean)\n    \"\"\"(bool): Is the certificate an identity certificate?\"\"\"\n    der_data = db.Column(db.LargeBinary, nullable=False)\n    \"\"\"(bytes): The DER encoded certificate data.\"\"\"\n    fingerprint_sha256 = db.Column(db.String(64), nullable=False, index=True)\n    \"\"\"(str): SHA-256 fingerprint of the certificate.\"\"\"\n\n\nclass InstalledProfile(db.Model):\n    \"\"\"This model represents a single installed profile on an enrolled device as returned by the ``ProfileList`` query.\n\n    The response does not contain the entire contents of the profiles installed therefore the UUIDs returned are joined\n    against our profiles table to ascertain whether profiles have been installed or not.\n\n    :table: installed_profiles\n\n    See Also:\n          - `ProfileList Command <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html#//apple_ref/doc/uid/TP40017387-CH3-SW7>`_.\n    \"\"\"\n    __tablename__ = 'installed_profiles'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"(int): Installed Profile ID\"\"\"\n    device_udid = db.Column(db.String(40), index=True, nullable=False)\n    \"\"\"(GUID): Unique Device Identifier\"\"\"\n    device_id = db.Column(db.ForeignKey('devices.id'), nullable=True)\n    \"\"\"(int): Device foreign key ID.\"\"\"\n    device = db.relationship('Device', backref='installed_profiles')\n    \"\"\"(db.relationship): Device relationship\"\"\"\n\n    has_removal_password = db.Column(db.Boolean)\n    \"\"\"(bool): Does the installed profile have a removal password?\"\"\"\n    is_encrypted = db.Column(db.Boolean)\n    \"\"\"(bool): Is the installed profile encrypted?\"\"\"\n    is_managed = db.Column(db.Boolean)\n    \"\"\"(bool): Is the installed profile managed? which means it has been sourced from the MDM.\"\"\"\n\n    payload_description = db.Column(db.String)\n    \"\"\"(str): Payload description (value of PayloadDescription)\"\"\"\n    payload_display_name = db.Column(db.String)\n    \"\"\"(str): Payload display name\"\"\"\n    payload_identifier = db.Column(db.String)\n    payload_organization = db.Column(db.String)\n    payload_removal_disallowed = db.Column(db.Boolean)\n    payload_uuid = db.Column(GUID, index=True)\n    # SignerCertificates\n\n\nclass InstalledPayload(db.Model):\n    __tablename__ = 'installed_payloads'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"(int): Installed Payload ID\"\"\"\n    profile_id = db.Column(db.ForeignKey('installed_profiles.id'), nullable=False)\n    \"\"\"(int): InstalledProfile foreign key ID.\"\"\"\n    profile = db.relationship('InstalledProfile', backref='payload_content')\n    device_id = db.Column(db.ForeignKey('devices.id'), nullable=True)\n    \"\"\"(int): Device foreign key ID.\"\"\"\n    device = db.relationship('Device', backref='installed_payloads')\n    \"\"\"(db.relationship): Device relationship\"\"\"\n\n    \"\"\"(db.relationship): InstalledProfile relationship\"\"\"\n    description = db.Column(db.String)\n    \"\"\"(str): Payload description (value of PayloadDescription)\"\"\"\n    display_name = db.Column(db.String)\n    \"\"\"(str): Payload display name\"\"\"\n    identifier = db.Column(db.String)\n    organization = db.Column(db.String)\n    payload_type = db.Column(db.String)\n    uuid = db.Column(GUID())\n\n\nclass AvailableOSUpdate(db.Model):\n    \"\"\"This table holds the results of `AvailableOSUpdates` commands.\"\"\"\n    __tablename__ = 'available_os_updates'\n\n    id = db.Column(db.Integer, primary_key=True)\n\n    device_id = db.Column(db.ForeignKey('devices.id'), nullable=True)\n    \"\"\"(int): Device foreign key ID.\"\"\"\n    device = db.relationship('Device', backref='available_os_updates')\n    \"\"\"(db.relationship): Device relationship\"\"\"\n\n    # Common to all platforms\n    allows_install_later = db.Column(db.Boolean)\n    human_readable_name = db.Column(db.String)\n    is_critical = db.Column(db.Boolean)\n    product_key = db.Column(db.String)\n    restart_required = db.Column(db.Boolean)\n    version = db.Column(db.String)\n\n    # macOS Only\n    app_identifiers_to_close = db.Column(MutableList.as_mutable(JSONEncodedDict))\n    human_readable_name_locale = db.Column(db.String)\n    is_config_data_update = db.Column(db.Boolean)\n    \"\"\"(bool): This update is a config data update eg. for XProtect or Gatekeeper. These arent normally shown\"\"\"\n    is_firmware_update = db.Column(db.Boolean)\n    metadata_url = db.Column(db.String)\n\n    # iOS Only\n    product_name = db.Column(db.String)\n    build = db.Column(db.String)\n    download_size = db.Column(db.BigInteger)\n    install_size = db.Column(db.BigInteger)\n"
  },
  {
    "path": "commandment/inventory/resources.py",
    "content": "from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship\nfrom flask_rest_jsonapi.exceptions import ObjectNotFound\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom commandment.inventory.schema import InstalledApplicationSchema, InstalledCertificateSchema, \\\n    InstalledProfileSchema, AvailableOSUpdateSchema\nfrom commandment.inventory.models import db, InstalledApplication, InstalledCertificate, InstalledProfile, \\\n    AvailableOSUpdate\nfrom commandment.models import Device\n\n\nclass InstalledApplicationsList(ResourceList):\n    def query(self, view_kwargs):\n        query_ = self.session.query(InstalledApplication)\n        if view_kwargs.get('device_id') is not None:\n            try:\n                self.session.query(Device).filter_by(id=view_kwargs['device_id']).one()\n            except NoResultFound:\n                raise ObjectNotFound({'parameter': 'device_id'}, \"Device: {} not found\".format(view_kwargs['device_id']))\n            else:\n                query_ = query_.join(Device).filter(Device.id == view_kwargs['device_id'])\n        return query_\n\n    schema = InstalledApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': InstalledApplication,\n        'methods': {'query': query}\n    }\n\n\nclass InstalledApplicationDetail(ResourceDetail):\n    schema = InstalledApplicationSchema\n    data_layer = {\n        'session': db.session,\n        'model': InstalledApplication\n    }\n\n\nclass InstalledCertificatesList(ResourceList):\n    def query(self, view_kwargs):\n        query_ = self.session.query(InstalledCertificate)\n        if view_kwargs.get('device_id') is not None:\n            try:\n                self.session.query(Device).filter_by(id=view_kwargs['device_id']).one()\n            except NoResultFound:\n                raise ObjectNotFound({'parameter': 'device_id'}, \"Device: {} not found\".format(view_kwargs['device_id']))\n            else:\n                query_ = query_.join(Device).filter(Device.id == view_kwargs['device_id'])\n        return query_\n\n    schema = InstalledCertificateSchema\n    view_kwargs = True\n    data_layer = {\n        'session': db.session,\n        'model': InstalledCertificate,\n        'methods': {'query': query}\n    }\n\n\nclass InstalledCertificateDetail(ResourceDetail):\n    schema = InstalledCertificateSchema\n    data_layer = {\n        'session': db.session,\n        'model': InstalledCertificate,\n        'url_field': 'installed_certificate_id'\n    }\n\n\n# class PayloadsList(ResourceList):\n#     schema = PayloadSchema\n#     data_layer = {\n#         'session': db.session,\n#         'model': Payload\n#     }\n#\n#\n# class PayloadDetail(ResourceDetail):\n#     schema = PayloadSchema\n#     data_layer = {\n#         'session': db.session,\n#         'model': Payload\n#     }\n\nclass InstalledProfilesList(ResourceList):\n    def query(self, view_kwargs):\n        query_ = self.session.query(InstalledProfile)\n        if view_kwargs.get('device_id') is not None:\n            try:\n                self.session.query(Device).filter_by(id=view_kwargs['device_id']).one()\n            except NoResultFound:\n                raise ObjectNotFound({'parameter': 'device_id'}, \"Device: {} not found\".format(view_kwargs['device_id']))\n            else:\n                query_ = query_.join(Device).filter(Device.id == view_kwargs['device_id'])\n        return query_\n\n    schema = InstalledProfileSchema\n    view_kwargs = True\n    data_layer = {\n        'session': db.session,\n        'model': InstalledProfile,\n        'methods': {'query': query}\n    }\n\n\nclass InstalledProfileDetail(ResourceDetail):\n    schema = InstalledProfileSchema\n    data_layer = {\n        'session': db.session,\n        'model': InstalledProfile\n    }\n\n\nclass AvailableOSUpdateList(ResourceList):\n    def query(self, view_kwargs):\n        query_ = self.session.query(AvailableOSUpdate)\n        if view_kwargs.get('device_id') is not None:\n            try:\n                self.session.query(Device).filter_by(id=view_kwargs['device_id']).one()\n            except NoResultFound:\n                raise ObjectNotFound({'parameter': 'device_id'}, \"Device: {} not found\".format(view_kwargs['device_id']))\n            else:\n                query_ = query_.join(Device).filter(Device.id == view_kwargs['device_id'])\n        return query_\n\n    schema = AvailableOSUpdateSchema\n    view_kwargs = True\n    data_layer = {\n        'session': db.session,\n        'model': AvailableOSUpdate,\n        'methods': {'query': query}\n    }\n\n\nclass AvailableOSUpdateDetail(ResourceDetail):\n    schema = AvailableOSUpdateSchema\n    data_layer = {\n        'session': db.session,\n        'model': AvailableOSUpdate,\n        'url_field': 'available_os_update_id'\n    }\n"
  },
  {
    "path": "commandment/inventory/schema.py",
    "content": "from marshmallow_jsonapi import fields\nfrom marshmallow_jsonapi.flask import Relationship, Schema\n\n\nclass InstalledProfileSchema(Schema):\n    class Meta:\n        type_ = 'installed_profiles'\n        self_view = 'api_app.installed_profile_detail'\n        self_view_kwargs = {'installed_profile_id': '<id>'}\n        self_view_many = 'api_app.installed_profiles_list'\n\n    id = fields.Int(dump_only=True)\n\n    has_removal_password = fields.Bool()\n    is_encrypted = fields.Bool()\n    payload_description = fields.Str()\n    payload_display_name = fields.Str()\n    payload_identifier = fields.Str()\n    payload_organization = fields.Str()\n    payload_removal_disallowed = fields.Boolean()\n    payload_uuid = fields.UUID()\n    # signer_certificates = fields.Nested()\n\n    device = Relationship(\n        related_view='api_app.device_detail',\n        related_view_kwargs={'device_id': '<device_id>'},\n        type_='devices',\n    )\n\n\nclass InstalledCertificateSchema(Schema):\n    class Meta:\n        type_ = 'installed_certificates'\n        self_view = 'api_app.installed_certificate_detail'\n        self_view_kwargs = {'installed_certificate_id': '<id>'}\n        self_view_many = 'api_app.installed_certificates_list'\n        strict = True\n\n    id = fields.Int(dump_only=True)\n    x509_cn = fields.Str(dump_only=True)\n    is_identity = fields.Boolean(dump_only=True)\n    fingerprint_sha256 = fields.String(dump_only=True)\n\n    device = Relationship(\n        related_view='api_app.device_detail',\n        related_view_kwargs={'device_id': '<device_id>'},\n        type_='devices',\n    )\n\n\nclass InstalledApplicationSchema(Schema):\n    class Meta:\n        type_ = 'installed_applications'\n        self_view = 'api_app.installed_application_detail'\n        self_view_kwargs = {'installed_application_id': '<id>'}\n        self_view_many = 'api_app.installed_applications_list'\n        strict = True\n\n    id = fields.Int(dump_only=True)\n    bundle_identifier = fields.Str(dump_only=True)\n    name = fields.Str(dump_only=True)\n    short_version = fields.Str(dump_only=True)\n    version = fields.Str(dump_only=True)\n    bundle_size = fields.Int(dump_only=True)\n    dynamic_size = fields.Int(dump_only=True)\n    is_validated = fields.Bool(dump_only=True)\n\n    device = Relationship(\n        related_view='api_app.device_detail',\n        related_view_kwargs={'device_id': '<device_id>'},\n        type_='devices',\n    )\n\n\nclass AvailableOSUpdateSchema(Schema):\n    class Meta:\n        type_ = 'available_os_updates'\n        self_view = 'api_app.available_os_update_detail'\n        self_view_kwargs = {'available_os_update_id': '<id>'}\n        self_view_many = 'api_app.available_os_updates_list'\n\n    id = fields.Int(dump_only=True)\n    allows_install_later = fields.Boolean()\n    #  app_identifiers_to_close = fields.List(fields.String())\n    human_readable_name = fields.Str()\n    human_readable_name_locale = fields.Str()\n    is_config_data_update = fields.Boolean()\n    is_critical = fields.Boolean()\n    is_firmware_update = fields.Boolean()\n    metadata_url = fields.URL()\n    product_key = fields.String()\n    restart_required = fields.Boolean()\n    version = fields.String()\n\n    device = Relationship(\n        related_view='api_app.device_detail',\n        related_view_kwargs={'device_id': '<device_id>'},\n        type_='devices',\n    )\n"
  },
  {
    "path": "commandment/mdm/__init__.py",
    "content": "from typing import Set\nfrom enum import IntFlag, auto, Enum, IntEnum\n\n\nclass CommandType(Enum):\n    ProfileList = 'ProfileList'\n    InstallProfile = 'InstallProfile'\n    RemoveProfile = 'RemoveProfile'\n    ProvisioningProfileList = 'ProvisioningProfileList'\n    InstallProvisioningProfile = 'InstallProvisioningProfile'\n    RemoveProvisioningProfile = 'RemoveProvisioningProfile'\n    CertificateList = 'CertificateList'\n    InstalledApplicationList = 'InstalledApplicationList'\n    DeviceInformation = 'DeviceInformation'\n    SecurityInfo = 'SecurityInfo'\n    DeviceLock = 'DeviceLock'\n    RestartDevice = 'RestartDevice'\n    ShutDownDevice = 'ShutDownDevice'\n    ClearPasscode = 'ClearPasscode'\n    EraseDevice = 'EraseDevice'\n    RequestMirroring = 'RequestMirroring'\n    StopMirroring = 'StopMirroring'\n    Restrictions = 'Restrictions'\n    ClearRestrictionsPasscode = 'ClearRestrictionsPasscode'\n\n    # Shared iPad\n    UserList = 'UserList'\n    UnlockUserAccount = 'UnlockUserAccount'\n    LogOutUser = 'LogOutUser'\n    DeleteUser = 'DeleteUser'\n\n    EnableLostMode = 'EnableLostMode'\n    PlayLostModeSound = 'PlayLostModeSound'\n    DisableLostMode = 'DisableLostMode'\n    DeviceLocation = 'DeviceLocation'\n\n    # Managed Applications\n    InstallApplication = 'InstallApplication'\n    ApplyRedemptionCode = 'ApplyRedemptionCode'\n    ManagedApplicationList = 'ManageApplicationList'\n    RemoveApplication = 'RemoveApplication'\n    InviteToProgram = 'InviteToProgram'\n    ValidateApplications = 'ValidateApplications'\n\n    # Books\n    InstallMedia = 'InstallMedia'\n    ManagedMediaList = 'ManagedMediaList'\n    RemoveMedia = 'RemoveMedia'\n\n    Settings = 'Settings'\n\n    ManagedApplicationConfiguration = 'ManagedApplicationConfiguration'\n    ApplicationConfiguration = 'ApplicationConfiguration'\n    ManagedApplicationAttributes = 'ManagedApplicationAttributes'\n    ManagedApplicationFeedback = 'ManagedApplicationFeedback'\n    AccountConfiguration = 'AccountConfiguration'\n\n    SetFirmwarePassword = 'SetFirmwarePassword'\n    VerifyFirmwarePassword = 'VerifyFirmwarePassword'\n\n    SetAutoAdminPassword = 'SetAutoAdminPassword'\n    DeviceConfigured = 'DeviceConfigured'\n    ScheduleOSUpdate = 'ScheduleOSUpdate'\n    ScheduleOSUpdateScan = 'ScheduleOSUpdateScan'\n    AvailableOSUpdates = 'AvailableOSUpdates'\n    OSUpdateStatus = 'OSUpdateStatus'\n\n    ActiveNSExtensions = 'ActiveNSExtensions'\n    NSExtensionMappings = 'NSExtensionMappings'\n    RotateFileVaultKey = 'RotateFileVaultKey'\n\n\nclass Platform(Enum):\n    \"\"\"The platform of the managed device.\"\"\"\n    Unknown = 'Unknown'  # Not enough information\n    macOS = 'macOS'\n    iOS = 'iOS'\n    tvOS = 'tvOS'\n\n\nclass AccessRights(IntFlag):\n    \"\"\"The MDM protocol defines a bitmask for granting permissions to an MDM to perform certain operations.\n    \n    This enumeration contains all of those access rights flags.\n    \"\"\"\n    def _generate_next_value_(name, start, count, last_values):\n        return 2 ** count\n\n    ProfileInspection = auto()\n    ProfileInstallRemove = auto()\n    DeviceLockPasscodeRemoval = auto()\n    DeviceErase = auto()\n    QueryDeviceInformation = auto()\n    QueryNetworkInformation = auto()\n    ProvProfileInspection = auto()\n    ProvProfileInstallRemove = auto()\n    InstalledApplications = auto()\n    RestrictionQueries = auto()\n    SecurityQueries = auto()\n    ChangeSettings = auto()\n    ManageApps = auto()\n\n    All = ProfileInspection | ProfileInstallRemove | DeviceLockPasscodeRemoval | DeviceErase | QueryDeviceInformation \\\n          | QueryNetworkInformation | ProvProfileInspection | ProvProfileInstallRemove | InstalledApplications \\\n          | RestrictionQueries | SecurityQueries | ChangeSettings | ManageApps\n\n\nAccessRightsSet = Set[AccessRights]\n\n\nclass CommandStatus(Enum):\n    \"\"\"CommandStatus describes all the possible states of a command in the device command queue.\n\n    The following statuses are based upon the return status of the MDM client:\n\n    - Acknowledged\n    - Error\n    - CommandFormatError\n    - NotNow\n\n    Additionally, there are statuses to explain the lifecycle of the command before and after the MDM client processes\n    them:\n\n    - Queued: The command was newly created and not yet sent to the device.\n    - Sent: The command has been sent to the device, but no response has come back yet.\n    - Expired: The command was never acknowledged, or the device was removed.\n    \"\"\"\n\n    # MDM Client Statuses\n    Idle = 'Idle'\n    Acknowledged = 'Acknowledged'\n    Error = 'Error'\n    CommandFormatError = 'CommandFormatError'\n    NotNow = 'NotNow'\n\n    # Commandment Lifecycle Statuses\n    Queued = 'Queued'\n    Sent = 'Sent'\n    Expired = 'Expired'\n\n\nclass SettingsItem(Enum):\n    \"\"\"A list of possible values for Managed Settings items.\n\n    See Also:\n          - `Managed Settings <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html#//apple_ref/doc/uid/TP40017387-CH3-SW59>`_._\n    \"\"\"\n    VoiceRoaming = 'VoiceRoaming'\n    PersonalHotspot = 'PersonalHotspot'\n    Wallpaper = 'Wallpaper'\n    DataRoaming = 'DataRoaming'\n    ApplicationAttributes = 'ApplicationAttributes'\n    DeviceName = 'DeviceName'\n    HostName = 'HostName'\n    MDMOptions = 'MDMOptions'\n    PasscodeLockGracePeriod = 'PasscodeLockGracePeriod'\n    MaximumResidentUsers = 'MaximumResidentUsers'\n\n\nclass WallpaperLocation(IntEnum):\n    \"\"\"A list of possible values for the Wallpaper `where` setting.\n\n    Determines where the given wallpaper will be used.\n    \"\"\"\n    LockScreen = 1\n    HomeScreen = 2\n    Both = 3\n\n\n"
  },
  {
    "path": "commandment/mdm/api.py",
    "content": "from flask import Blueprint\nfrom commandment.mdm.resources import CommandsList, CommandDetail\nfrom commandment.api.app_jsonapi import api\n\napi_app = Blueprint('inventory_api_app', __name__)\n\n# Commands\napi.route(CommandsList, 'commands_list', '/v1/commands', '/v1/devices/<int:device_id>/commands')\napi.route(CommandDetail, 'command_detail', '/v1/commands/<int:command_id>')\n"
  },
  {
    "path": "commandment/mdm/app.py",
    "content": "\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2017 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\"\"\"\nfrom flask import Blueprint, make_response, abort, jsonify, g, current_app\nfrom sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound\nfrom commandment.mdm import CommandStatus\nfrom commandment.mdm.commands import Command\nfrom commandment.decorators import parse_plist_input_data\nfrom commandment.cms.decorators import verify_mdm_signature\nfrom commandment.mdm.util import queue_full_inventory\nfrom commandment.models import DeviceUser\nfrom commandment.pki.models import DeviceIdentityCertificate\nfrom commandment.mdm.routers import CommandRouter, PlistRouter\nfrom commandment.utils import plistify\nimport plistlib\nimport ssl\nfrom commandment.apns.push import push_to_device\nfrom datetime import datetime\nfrom commandment.signals import device_enrolled\n\n\nmdm_app = Blueprint('mdm_app', __name__)\n\nplr = PlistRouter(mdm_app, '/checkin')\ncommand_router = CommandRouter(mdm_app)\nfrom .handlers import *\n\n\n@plr.route('MessageType', 'Authenticate')\ndef authenticate(plist_data):\n    \"\"\"Handle the `Authenticate` message.\n    \n    This will be the first message sent to the MDM upon enrollment, but you cannot consider the device to be enrolled\n    at this stage.\n    \"\"\"\n    current_app.logger.debug('Authenticate (UDID %s)', plist_data.get('UDID', None))\n    # TODO: check to make sure device == UDID == cert, etc.\n    try:\n        device = db.session.query(Device).filter(Device.udid == plist_data['UDID']).one()\n    except NoResultFound:\n        # no device found, let's make a new one!\n        device = Device()\n        db.session.add(device)\n\n        device.udid = plist_data['UDID']\n        device.build_version = plist_data.get('BuildVersion')\n        device.device_name = plist_data.get('DeviceName')\n        device.model = plist_data.get('Model')\n        device.model_name = plist_data.get('ModelName')\n        device.os_version = plist_data.get('OSVersion')\n        device.product_name = plist_data.get('ProductName')\n        device.serial_number = plist_data.get('SerialNumber')\n        device.topic = plist_data.get('Topic')\n\n        # iOS only\n        device.imei = plist_data.get('IMEI', None)\n        device.meid = plist_data.get('MEID', None)\n\n        device.last_seen = datetime.now()\n\n    # Authenticate message is not enough to be enrolled\n    device.is_enrolled = False\n\n    # remove the previous device token (in the case of a re-enrollment) to\n    # tell the difference between a periodic TokenUpdate and the first\n    # post-enrollment TokenUpdate\n    device.token = None\n\n    # TODO: Check supplied identity against identities we actually issued\n\n    db.session.commit()\n\n    return 'OK'\n\n\n@plr.route('MessageType', 'TokenUpdate')\n@verify_mdm_signature\ndef token_update(plist_data):\n    current_app.logger.debug('TokenUpdate (UDID %s)', plist_data.get('UDID', None))\n    try:\n        device = db.session.query(Device).filter(Device.udid == plist_data['UDID']).one()\n    except NoResultFound:\n        current_app.logger.debug(\n            'Device (UDID: %s) will be unenrolled because the database has no record of this device.', plist_data['UDID'])\n        return abort(410)  # Ask the device to unenroll itself because we dont seem to have any records.\n\n    # TODO: a TokenUpdate can either be for a device or a user (per OS X extensions)\n    if 'UserID' in plist_data:\n        device_user = DeviceUser(\n            \n        )\n        return 'OK'\n\n    if not device.token:  # First contact\n        device.is_enrolled = True\n\n        if hasattr(g, 'signers'):\n            device_certificate = DeviceIdentityCertificate.from_crypto(g.signers[0])\n            db.session.add(device_certificate)\n            device.certificate = device_certificate\n        else:\n            pass  # TODO: if in debug mode this should not throw an exception to deal with cert troubleshooting\n\n        device_enrolled.send(device)\n        queue_full_inventory(device)\n\n    device.tokenupdate_at = datetime.utcnow()\n    device.push_magic = plist_data['PushMagic']\n    device.topic = plist_data['Topic']\n    device.token = plist_data['Token']\n    device.unlock_token = plist_data.get('UnlockToken', None)\n    device.last_seen = datetime.now()\n    db.session.commit()\n\n    try:\n        response = push_to_device(device)\n    except ssl.SSLError:\n        return abort(jsonify(error=True, message=\"The push certificate has expired\"))\n\n    current_app.logger.info(\"[APNS2 Response] Status: %d, Reason: %s, APNS ID: %s, Timestamp\",\n                            response.status_code, response.reason, response.apns_id.decode('utf-8'))\n    device.last_push_at = datetime.utcnow()\n    if response.status_code == 200:\n        device.last_apns_id = response.apns_id\n\n    db.session.commit()\n\n    # TODO: macOS can chain commands from TokenUpdate but iOS will not process any response data from TokenUpdate.\n    # Therefore, it is necessary to issue a Push here instead of simply responding with the next command.\n    return 'OK'\n\n\n@plr.route('MessageType', 'UserAuthenticate')\ndef user_authenticate(plist_data):\n    abort(410, 'per-user authentication not yet supported')\n\n\n@plr.route('MessageType', 'CheckOut')\ndef check_out(plist_data):\n    \"\"\"Handle the `CheckOut` message.\n    \n    Todo:\n        - Handle CheckOuts for the user channel.\n    \"\"\"\n    device_udid = plist_data['UDID']\n    try:\n        d = db.session.query(Device).filter(Device.udid == device_udid).one()\n    except NoResultFound:\n        current_app.logger.warning('Attempted to unenroll device with UDID: {}, but none was found'.format(device_udid))\n        return abort(404, 'No matching device found')\n\n    except MultipleResultsFound:\n        current_app.logger.warning(\n            'Attempted to unenroll device with UDID: {}, but there were multiple, check your database'.format(device_udid))\n        return abort(500, 'Too many devices matching')\n\n    d.last_seen = datetime.utcnow()\n    d.is_enrolled = False\n\n    # Make sure we cant even accidentally push to an invalid relationship\n    d.token = None\n    d.push_magic = None\n\n    db.session.commit()\n    current_app.logger.debug('Device has been unenrolled, UDID: {}'.format(device_udid))\n\n    return 'OK'\n\n\n@mdm_app.route(\"/mdm\", methods=['PUT'])\n@verify_mdm_signature\n@parse_plist_input_data\ndef mdm():\n    \"\"\"MDM connection endpoint.\n\n    Most MDM communication is via this URI.\n\n    This endpoint delivers and handles incoming command responses.\n    Such as: `Idle`, `NotNow`, `Acknowledged`.\n\n    :reqheader Content-Type: application/x-apple-aspen-mdm; charset=UTF-8\n    :reqheader Mdm-Signature: BASE64-encoded CMS Detached Signature of the message. (if `SignMessage` was true)\n    :resheader Content-Type: application/xml; charset=UTF-8\n    :status 200: With an empty body, no commands remaining, or plist contents of next command.\n    :status 400: Invalid data submitted\n    :status 410: User channel capability not available.\n    \"\"\"\n    # TODO: proper identity verification, for now just matching on UDID\n    try:\n        device = db.session.query(Device).filter(Device.udid == g.plist_data['UDID']).one()\n    except NoResultFound:\n        current_app.logger.info(\"An unmanaged device (UDID %s), tried to check in with us, rejecting.\", g.plist_data['UDID'])\n        return abort(410)  # Unmanage devices that we dont have a record of\n\n    if 'UserID' in g.plist_data:\n        # Note that with DEP this is an opportune time to queue up an \n        # application install for the /device/ despite this being a per-user\n        # MDM command. this is becasue DEP appears to only allow apps to be\n        # installed while a user is logged in. note also the undocumented\n        # NotOnConsole key to (possibly) indicate that this is a UI login?\n        current_app.logger.warn('per-user MDM command not yet supported')\n        return ''\n\n    if 'Status' not in g.plist_data:\n        current_app.logger.error('invalid MDM request (no Status provided) from device id %d' % device.id)\n        return abort(400, 'response does not contain Status')\n    else:\n        status = CommandStatus(g.plist_data['Status'])\n\n    current_app.logger.info('device id=%d udid=%s processing status=%s', device.id, device.udid, status)\n    device.last_seen = datetime.utcnow()\n    db.session.commit()\n\n    if current_app.config['DEBUG']:\n        try:\n            print(g.plist_data)\n        except UnicodeEncodeError:\n            print('Cannot DEBUG print plist request, unencodable characters')\n\n    if status != CommandStatus.Idle:  # this device is responding to an earlier command.\n        if 'CommandUUID' not in g.plist_data:\n            current_app.logger.error('missing CommandUUID for non-Idle status')\n            abort(400, 'response does not contain CommandUUID')\n        try:\n            command = DBCommand.find_by_uuid(g.plist_data['CommandUUID'])\n            command.status = status\n            command.acknowledged_at = datetime.utcnow()\n            db.session.commit()\n\n            # Re-hydrate the command class based on the persisted model containing the request type and the parameters\n            # that were given to generate the command\n            # turns out this is less useful than passing the db model\n            # cmd = Command.new_request_type(command.request_type, command.parameters, command.uuid)\n\n            # route the response by the handler type corresponding to that command\n            command_router.handle(command, device, g.plist_data)\n\n        except NoResultFound:\n            current_app.logger.warning('no record of command uuid=%s', g.plist_data['CommandUUID'])\n\n    if status == CommandStatus.NotNow:\n        current_app.logger.warn('NotNow status received, command will backoff')  # TODO: exponential backoff\n\n    command = DBCommand.next_command(device)\n\n    if not command:\n        current_app.logger.info('no further MDM commands for device=%d', device.id)\n        return ''\n\n    # mark this command as being in process right away to (try) to avoid\n    # any race conditions with mutliple MDM commands from the same device\n    # at a time\n\n    #command.set_processing()\n    #db.session.commit()\n\n    # Re-hydrate the command class based on the persisted model containing the request type and the parameters\n    # that were given to generate the command\n    cmd = Command.new_request_type(command.request_type, command.parameters, command.uuid)\n\n\n    # get command dictionary representation (e.g. the full command to send)\n    output_dict = cmd.to_dict()\n\n    current_app.logger.info('sending %s MDM command class=%s to device=%d', cmd.request_type,\n                            command.request_type, device.id)\n\n    current_app.logger.debug(output_dict)\n\n    command.status = CommandStatus.Sent\n    command.sent_at = datetime.utcnow()\n    db.session.commit()\n\n    return plistify(output_dict)\n\n\n"
  },
  {
    "path": "commandment/mdm/commands.py",
    "content": "from enum import Enum\nfrom uuid import uuid4, UUID\nfrom typing import Dict, Set, List, Type, ClassVar, Any, Optional, Tuple\nimport semver\nfrom base64 import urlsafe_b64encode, urlsafe_b64decode\nfrom . import AccessRights, AccessRightsSet, Platform\n\nPlatformVersion = str\nPlatformRequirements = Dict[Platform, PlatformVersion]\n\n\nclass CommandRegistry(type):\n    command_classes: Dict[str, Type] = {}\n\n    def __new__(mcs, name, bases, namespace, **kwds):\n        ns = dict(namespace)\n        klass = type.__new__(mcs, name, bases, ns)\n        if 'request_type' in ns:\n            CommandRegistry.command_classes[ns['request_type']] = klass\n\n        return klass\n\n\nclass Command(metaclass=CommandRegistry):\n\n    # request_type: ClassVar[str] = None\n    \"\"\"request_type (str): The MDM RequestType, as specified in the MDM Specification.\"\"\"\n\n    # require_access: ClassVar[AccessRightsSet] = set()\n    \"\"\"require_access (Set[AccessRights]): Access required for the MDM to execute the command on this device.\"\"\"\n\n    # require_platforms: ClassVar[PlatformRequirements] = dict()\n    \"\"\"require_platforms (PlatformRequirements): A dict of Platform : version predicate string, to indicate which \n    platforms will accept the command\"\"\"\n\n    # require_supervised: ClassVar[bool] = False\n    \"\"\"require_supervised (bool): This command requires supervision on iOS/tvOS\"\"\"\n\n    def __init__(self, uuid=None) -> None:\n        \"\"\"The Command class wraps an MDM Request Command dict to provide validation and convenience methods for\n        accessing command attributes.\n\n        All commands are serialised to the same table as JSON, so the validation is performed here.\n\n        Args:\n            uuid (UUID): The command uuid. Defaults to an automatically generated uuid.\n        \"\"\"\n        if uuid is None:\n            uuid = uuid4()\n\n        self._uuid: UUID = uuid\n        self._attrs: Dict[str, Any] = {}\n        # self.request_type: Optional[str] = None\n        # self.require_access: AccessRightsSet = set()\n        # self.require_platforms: PlatformRequirements = dict()\n        # self.require_supervised: bool = False\n\n    @property\n    def uuid(self) -> UUID:\n        return self._uuid\n\n    @property\n    def parameters(self) -> Dict[str, Any]:\n        return self._attrs\n\n    @classmethod\n    def new_request_type(cls, request_type: str, parameters: dict, uuid: str = None) -> 'Command':\n        \"\"\"Factory method for instantiating a command based on its class attribute ``request_type``.\n\n        Additionally, the dict given in parameters will be applied to the command instance.\n        Commands that have no parameters are not required to implement to_dict().\n\n        Args:\n              request_type (str): The command request type, as defined in the class attribute ``request_type``.\n              parameters (dict): The parameters of this command instance.\n              uuid (str): The command UUID. Optional, will be generated if omitted.\n        Raises:\n              ValueError if there is no command matching the request type given.\n        Returns:\n              Command class that corresponds to the request type given. Inherits from Command.\n        \"\"\"\n        if request_type in CommandRegistry.command_classes:\n            klass = CommandRegistry.command_classes[request_type]\n            return klass(uuid, **parameters)\n        else:\n            raise ValueError('No such RequestType registered: {}'.format(request_type))\n\n    def to_dict(self) -> dict:\n        \"\"\"Convert the command into a dict that will be serializable by plistlib.\n\n        This default implementation will work for command types that have no parameters.\n        \"\"\"\n        command = {'RequestType': self.request_type}\n\n        return {\n            'CommandUUID': str(self._uuid),\n            'Command': command,\n        }\n\n\nclass DeviceInformation(Command):\n    request_type = 'DeviceInformation'\n    require_access = {AccessRights.QueryDeviceInformation, AccessRights.QueryNetworkInformation}\n\n    class Queries(Enum):\n        \"\"\"The Queries enumeration contains all possible Query types for the DeviceInformation command.\"\"\"\n\n        # Table 5 : General Queries\n        UDID = 'UDID'\n        Languages = 'Languages'\n        Locales = 'Locales'\n        DeviceID = 'DeviceID'\n        OrganizationInfo = 'OrganizationInfo'\n        LastCloudBackupDate = 'LastCloudBackupDate'\n        AwaitingConfiguration = 'AwaitingConfiguration'\n        AutoSetupAdminAccounts = 'AutoSetupAdminAccounts'\n\n        # Table 6 : iTunes Account\n        iTunesStoreAccountIsActive = 'iTunesStoreAccountIsActive'\n        iTunesStoreAccountHash = 'iTunesStoreAccountHash'\n\n        # Table 7 : Device Queries\n        DeviceName = 'DeviceName'\n        OSVersion = 'OSVersion'\n        BuildVersion = 'BuildVersion'\n        ModelName = 'ModelName'\n        Model = 'Model'\n        ProductName = 'ProductName'\n        SerialNumber = 'SerialNumber'\n        DeviceCapacity = 'DeviceCapacity'\n        AvailableDeviceCapacity = 'AvailableDeviceCapacity'\n        BatteryLevel = 'BatteryLevel'\n        CellularTechnology = 'CellularTechnology'\n        IMEI = 'IMEI'\n        MEID = 'MEID'\n        ModemFirmwareVersion = 'ModemFirmwareVersion'\n        IsSupervised = 'IsSupervised'\n        IsDeviceLocatorServiceEnabled = 'IsDeviceLocatorServiceEnabled'\n        IsActivationLockEnabled = 'IsActivationLockEnabled'\n        IsDoNotDisturbInEffect = 'IsDoNotDisturbInEffect'\n        EASDeviceIdentifier = 'EASDeviceIdentifier'\n        IsCloudBackupEnabled = 'IsCloudBackupEnabled'\n        OSUpdateSettings = 'OSUpdateSettings'\n        LocalHostName = 'LocalHostName'\n        HostName = 'HostName'\n        SystemIntegrityProtectionEnabled = 'SystemIntegrityProtectionEnabled'\n        ActiveManagedUsers = 'ActiveManagedUsers'\n        IsMDMLostModeEnabled = 'IsMDMLostModeEnabled'\n        MaximumResidentUsers = 'MaximumResidentUsers'\n\n        # Table 9 : Network Information Queries\n        ICCID = 'ICCID'\n        BluetoothMAC = 'BluetoothMAC'\n        WiFiMAC = 'WiFiMAC'\n        EthernetMACs = 'EthernetMACs'\n        CurrentCarrierNetwork = 'CurrentCarrierNetwork'\n        SIMCarrierNetwork = 'SIMCarrierNetwork'\n        SubscriberCarrierNetwork = 'SubscriberCarrierNetwork'\n        CarrierSettingsVersion = 'CarrierSettingsVersion'\n        PhoneNumber = 'PhoneNumber'\n        VoiceRoamingEnabled = 'VoiceRoamingEnabled'\n        DataRoamingEnabled = 'DataRoamingEnabled'\n        IsRoaming = 'IsRoaming'\n        PersonalHotspotEnabled = 'PersonalHotspotEnabled'\n        SubscriberMCC = 'SubscriberMCC'\n        SubscriberMNC = 'SubscriberMNC'\n        CurrentMCC = 'CurrentMCC'\n        CurrentMNC = 'CurrentMNC'\n\n        # Maybe undocumented\n        CurrentConsoleManagedUser = 'CurrentConsoleManagedUser'\n\n    Requirements = {\n        'Languages': [\n            (Platform.iOS, '>=7'),\n            (Platform.tvOS, '>=6'),\n            (Platform.macOS, '>=10.10'),\n        ],\n        'Locales': [\n            (Platform.iOS, '>=7'),\n            (Platform.tvOS, '>=6'),\n            (Platform.macOS, '>=10.10'),\n        ],\n        'DeviceID': [\n            (Platform.tvOS, '>=6'),\n        ],\n        'OrganizationInfo': [\n            (Platform.iOS, '>=7'),\n        ],\n        'LastCloudBackupDate': [\n            (Platform.iOS, '>=8'),\n            (Platform.macOS, '>=10.10')\n        ],\n        'AwaitingConfiguration': [\n            (Platform.iOS, '>=9'),\n        ],\n        'AutoSetupAdminAccounts': [\n            (Platform.macOS, '>=10.11')\n        ],\n        'BatteryLevel': [\n            (Platform.iOS, '>=5')\n        ],\n        'CellularTechnology': [\n            (Platform.iOS, '>=4.2.6')\n        ],\n        'iTunesStoreAccountIsActive': [\n            (Platform.iOS, '>=7'),\n            (Platform.macOS, '>=10.9')\n        ],\n        'iTunesStoreAccountHash': [\n            (Platform.iOS, '>=8'),\n            (Platform.macOS, '>=10.10')\n        ],\n        'IMEI': [\n            (Platform.iOS, '*'),\n        ],\n        'MEID': [\n            (Platform.iOS, '*'),\n        ],\n        'ModemFirmwareVersion': [\n            (Platform.iOS, '*'),\n        ],\n        'IsSupervised': [\n            (Platform.iOS, '>=6'),\n        ],\n        'IsDeviceLocatorServiceEnabled': [\n            (Platform.iOS, '>=7'),\n        ],\n        'IsActivationLockEnabled': [\n            (Platform.iOS, '>=7'),\n            (Platform.macOS, '>=10.9')\n        ],\n        'IsDoNotDisturbInEffect': [\n            (Platform.iOS, '>=7'),\n        ],\n        'EASDeviceIdentifier': [\n            (Platform.iOS, '>=7'),\n            (Platform.macOS, '>=10.9'),\n        ],\n        'IsCloudBackupEnabled': [\n            (Platform.iOS, '>=7.1'),\n        ],\n        'OSUpdateSettings': [\n            (Platform.macOS, '>=10.11'),\n        ],\n        'LocalHostName': [\n            (Platform.macOS, '>=10.11'),\n        ],\n        'HostName': [\n            (Platform.macOS, '>=10.11'),\n        ],\n        'SystemIntegrityProtectionEnabled': [\n            (Platform.macOS, '>=10.12'),\n        ],\n        'ActiveManagedUsers': [\n            (Platform.macOS, '>=10.11'),\n        ],\n        'IsMDMLostModeEnabled': [\n            (Platform.iOS, '>=9.3'),\n        ],\n        'MaximumResidentUsers': [\n            (Platform.iOS, '>=9.3'),\n        ]\n    }\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(DeviceInformation, self).__init__(uuid)\n        self._attrs = kwargs\n\n    @classmethod\n    def for_platform(cls, platform: Platform, min_os_version: str, queries: Set[Queries] = None) -> 'DeviceInformation':\n        \"\"\"Generate a command that is compatible with the specified platform and OS version.\n\n        Args:\n              platform (Platform): Desired target platform\n              min_os_version (str): Desired OS version\n              queries (Set[Queries]): Desired Queries, or default to ALL queries.\n\n        Returns:\n              DeviceInformation instance with supported queries.\n        \"\"\"\n\n        def supported(query) -> bool:\n            if query not in cls.Requirements:\n                return True\n\n            platforms = cls.Requirements[query]\n            for req_platform, req_min_version in platforms:\n                if req_platform != platform:\n                    continue\n\n                # TODO: version checking\n                return True  # semver only takes maj.min.patch\n                #return semver.match(min_os_version, req_min_version)\n\n            return False\n\n        if queries is None:\n            supported_queries = filter(supported, [q.value for q in cls.Queries])\n        else:\n            supported_queries = filter(supported, queries)\n\n        return cls(Queries=list(supported_queries))\n\n    @property\n    def queries(self) -> Set[str]:\n        return self._attrs.get('Queries')\n\n    def to_dict(self) -> dict:\n        \"\"\"Convert the command into a dict that will be serializable by plistlib.\"\"\"\n        return {\n            'CommandUUID': str(self._uuid),\n            'Command': {\n                'RequestType': type(self).request_type,\n                'Queries': self._attrs.get('Queries', None),\n            }\n        }\n\n\nclass SecurityInfo(Command):\n    request_type = 'SecurityInfo'\n    require_access = {AccessRights.SecurityQueries}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(SecurityInfo, self).__init__(uuid)\n        self._attrs = kwargs\n\n\nclass DeviceLock(Command):\n    request_type = 'DeviceLock'\n    require_access = {AccessRights.DeviceLockPasscodeRemoval}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(DeviceLock, self).__init__(uuid)\n        self._attrs = kwargs\n\n    def to_dict(self) -> dict:\n        command = {\n            'RequestType': type(self).request_type,\n            'Message': self._attrs.get('Message', 'Device is locked'),\n        }\n\n        if 'PIN' in self._attrs:\n            command['PIN'] = self._attrs['PIN']\n\n        if 'PhoneNumber' in self._attrs:\n            command['PhoneNumber'] = self._attrs['PhoneNumber']\n\n        return {\n            'CommandUUID': str(self._uuid),\n            'Command': command,\n        }\n\n\nclass ClearPasscode(Command):\n    request_type = 'ClearPasscode'\n    require_access = {AccessRights.DeviceLockPasscodeRemoval}\n    require_platforms = {Platform.iOS: '*'}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(ClearPasscode, self).__init__(uuid)\n        self._attrs = kwargs\n\n    def to_dict(self) -> dict:\n        return {\n            'CommandUUID': str(self._uuid),\n            'Command': {\n                'RequestType': type(self).request_type,\n                'UnlockToken': urlsafe_b64decode(self._attrs['UnlockToken'])\n            }\n        }\n\n\nclass ProfileList(Command):\n    request_type = 'ProfileList'\n    require_access = {AccessRights.ProfileInspection}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(ProfileList, self).__init__(uuid)\n        self._attrs = kwargs\n\n\nclass InstallProfile(Command):\n    request_type = 'InstallProfile'\n    require_access = {AccessRights.ProfileInstallRemove}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(InstallProfile, self).__init__(uuid)\n        self._attrs = kwargs\n\n        if 'profile' in kwargs:\n            profile_data = kwargs['profile'].data\n            self._attrs['Payload'] = urlsafe_b64encode(profile_data).decode('utf-8')\n            del self._attrs['profile']\n\n    def to_dict(self) -> dict:\n        return {\n            'CommandUUID': str(self._uuid),\n            'Command': {\n                'RequestType': type(self).request_type,\n                'Payload': urlsafe_b64decode(self._attrs['Payload']),\n            }\n        }\n\n\nclass RemoveProfile(Command):\n    request_type = 'RemoveProfile'\n    require_access = {AccessRights.ProfileInstallRemove}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(RemoveProfile, self).__init__(uuid)\n        self._attrs = {\n            'Identifier': kwargs.get('Identifier')\n        }\n\n    def to_dict(self) -> dict:\n        \"\"\"Convert the command into a dict that will be serializable by plistlib.\"\"\"\n        return {\n            'CommandUUID': str(self._uuid),\n            'Command': {\n                'RequestType': type(self).request_type,\n                'Identifier': self._attrs.get('Identifier', None),\n            }\n        }\n\n\nclass CertificateList(Command):\n    request_type = 'CertificateList'\n    require_access = {AccessRights.ProfileInspection}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(CertificateList, self).__init__(uuid)\n        self._attrs = kwargs\n\n\nclass ProvisioningProfileList(Command):\n    request_type = 'ProvisioningProfileList'\n    require_access = {AccessRights.ProfileInspection}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs):\n        super(ProvisioningProfileList, self).__init__(uuid)\n        self._attrs = kwargs\n\n\nclass InstalledApplicationList(Command):\n    request_type = 'InstalledApplicationList'\n    require_access: Set[AccessRights] = set()\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs):\n        super(InstalledApplicationList, self).__init__(uuid)\n        self._attrs = {}\n        self._attrs.update(kwargs)\n\n    @property\n    def managed_apps_only(self) -> Optional[bool]:\n        return self._attrs.get('ManagedAppsOnly', None)\n\n    @managed_apps_only.setter\n    def managed_apps_only(self, value: bool) -> None:\n        self._attrs['ManagedAppsOnly'] = value\n\n    @property\n    def identifiers(self) -> Optional[List[str]]:\n        return self._attrs.get('Identifiers', None)\n\n    @identifiers.setter\n    def identifiers(self, bundle_ids: List[str]) -> None:\n        \"\"\"NOTE: setting identifiers for macOS 10.12 causes an exception in mdmclient.\"\"\"\n        self._attrs['Identifiers'] = bundle_ids\n\n    def to_dict(self) -> dict:\n        \"\"\"Convert the command into a dict that will be serializable by plistlib.\"\"\"\n        command = self._attrs\n        command.update({'RequestType': type(self).request_type})\n\n        return {\n            'CommandUUID': str(self._uuid),\n            'Command': command,\n        }\n\n\nclass InstallApplication(Command):\n    request_type = 'InstallApplication'\n    require_access = {AccessRights.ManageApps}\n\n    def __init__(self, uuid: Optional[UUID]=None, **kwargs) -> None:\n        super(InstallApplication, self).__init__(uuid)\n        self._attrs = {}\n        if 'application' in kwargs:\n            app = kwargs['application']\n            self._attrs['iTunesStoreID'] = app.itunes_store_id\n            self._attrs['ManagementFlags'] = 1\n            self._attrs['ChangeManagementState'] = 'Managed'\n        else:\n            self._attrs.update(kwargs)\n\n    @property\n    def itunes_store_id(self) -> Optional[int]:\n        return self._attrs.get('iTunesStoreID', None)\n\n    @itunes_store_id.setter\n    def itunes_store_id(self, id: int):\n        self._attrs['iTunesStoreID'] = id\n        if 'Options' not in self._attrs:\n            self._attrs['Options'] = {}\n            if 'PurchaseMethod' not in self._attrs['Options']:\n                self._attrs['Options']['PurchaseMethod'] = 1\n\n    def to_dict(self) -> dict:\n        cmd = super(InstallApplication, self).to_dict()\n        cmd['Command'].update(self._attrs)\n        print(cmd)\n        return cmd\n\n\nclass ManagedApplicationList(Command):\n    request_type = 'ManagedApplicationList'\n    require_access = {AccessRights.ManageApps}\n\n\nclass RestartDevice(Command):\n    request_type = 'RestartDevice'\n    require_access = {AccessRights.DeviceLockPasscodeRemoval}\n    require_platforms = {Platform.iOS: '>=10.3'}\n\n\nclass ShutDownDevice(Command):\n    request_type = 'ShutDownDevice'\n    require_access = {AccessRights.DeviceLockPasscodeRemoval}\n    require_platforms = {Platform.iOS: '>=10.3', Platform.macOS: '>=10.13'}\n\n\nclass EraseDevice(Command):\n    request_type = 'EraseDevice'\n    require_access = {AccessRights.DeviceErase}\n    require_platforms = {Platform.iOS: '*', Platform.macOS: '>=10.8'}\n\n\nclass RequestMirroring(Command):\n    request_type = 'RequestMirroring'\n    require_platforms = {Platform.iOS: '>=7', Platform.macOS: '>=10.10'}\n\n\nclass StopMirroring(Command):\n    request_type = 'StopMirroring'\n    require_platforms = {Platform.iOS: '>=7', Platform.macOS: '>=10.10'}\n    require_supervised = True\n\n\nclass Restrictions(Command):\n    request_type = 'Restrictions'\n    require_access = {AccessRights.RestrictionQueries, AccessRights.ProfileInspection}\n\n\nclass UsersList(Command):\n    request_type = 'UsersList'\n    require_platforms = {Platform.iOS: '>=9.3'}\n\n\nclass LogOutUser(Command):\n    request_type = 'LogOutUser'\n    require_platforms = {Platform.iOS: '>=9.3'}\n\n\nclass DeleteUser(Command):\n    request_type = 'DeleteUser'\n    require_platforms = {Platform.iOS: '>=9.3'}\n\n\nclass EnableLostMode(Command):\n    request_type = 'EnableLostMode'\n    require_platforms = {Platform.iOS: '>=9.3'}\n    require_supervised = True\n\n\nclass DisableLostMode(Command):\n    request_type = 'DisableLostMode'\n    require_platforms = {Platform.iOS: '>=9.3'}\n    require_supervised = True\n\n\nclass DeviceLocation(Command):\n    request_type = 'DeviceLocation'\n    require_platforms = {Platform.iOS: '>=9.3'}\n    require_supervised = True\n\n\nclass PlayLostModeSound(Command):\n    request_type = 'PlayLostModeSound'\n    require_platforms = {Platform.iOS: '>=10.3'}\n    require_supervised = True\n\n\nclass AvailableOSUpdates(Command):\n    request_type = 'AvailableOSUpdates'\n    require_platforms = {Platform.macOS: '>=10.11', Platform.iOS: '>=4'}\n\n\nclass Settings(Command):\n    request_type = 'Settings'\n    require_platforms = {Platform.macOS: '>=10.9', Platform.iOS: '>=5.0'}\n    require_access = {AccessRights.ChangeSettings}\n\n    def __init__(self,\n                 uuid: Optional[UUID]=None,\n                 device_name: Optional[str]=None,\n                 hostname: Optional[str]=None,\n                 voice_roaming: Optional[bool]=None,\n                 personal_hotspot: Optional[bool]=None,\n                 wallpaper=None,\n                 data_roaming: Optional[bool]=None,\n                 bluetooth: Optional[bool]=None,\n                 **kwargs) -> None:\n        super(Settings, self).__init__(uuid)\n        if 'settings' in kwargs:\n            self._attrs['settings'] = kwargs['settings']\n        else:\n            self._attrs['settings']: List[Dict[str, Any]] = []\n\n        if device_name is not None:\n            self._attrs['settings'].append({\n                'Item': 'DeviceName',\n                'DeviceName': device_name,\n            })\n\n        if hostname is not None:\n            self._attrs['settings'].append({\n                'Item': 'HostName',\n                'HostName': hostname,\n            })\n\n        if voice_roaming is not None:\n            self._attrs['settings'].append({\n                'Item': 'VoiceRoaming',\n                'Enabled': voice_roaming,\n            })\n\n        if personal_hotspot is not None:\n            self._attrs['settings'].append({\n                'Item': 'PersonalHotspot',\n                'Enabled': personal_hotspot,\n            })\n\n        if data_roaming is not None:\n            self._attrs['settings'].append({\n                'Item': 'DataRoaming',\n                'Enabled': data_roaming,\n            })\n\n        if bluetooth is not None:\n            self._attrs['settings'].append({\n                'Item': 'Bluetooth',\n                'Enabled': bluetooth,\n            })\n\n    def to_dict(self) -> dict:\n        return {\n            'CommandUUID': str(self._uuid),\n            'Command': {\n                'RequestType': type(self).request_type,\n                'Settings': self._attrs['settings'],\n            }\n        }\n"
  },
  {
    "path": "commandment/mdm/decorators.py",
    "content": "from functools import wraps\n\n\ndef handle_error_status(func):\n    \"\"\"This decorator looks at the request for an Error status, then handles the error accordingly:\n\n    \"\"\"\n    @wraps(func)\n    def handler(*args, **kwargs):\n        return func(*args, **kwargs)\n    return handler\n\n\n"
  },
  {
    "path": "commandment/mdm/handlers.py",
    "content": "from binascii import hexlify\n\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom flask import current_app\nfrom sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound\n\nfrom commandment.apps import ManagedAppStatus\nfrom commandment.apps.models import ManagedApplication\nfrom commandment.mdm import commands\nfrom commandment.mdm.app import command_router\nfrom .commands import ProfileList, DeviceInformation, SecurityInfo, InstalledApplicationList, CertificateList, \\\n    InstallProfile, AvailableOSUpdates, InstallApplication, RemoveProfile, ManagedApplicationList\nfrom .response_schema import InstalledApplicationListResponse, DeviceInformationResponse, AvailableOSUpdateListResponse, \\\n    ProfileListResponse, SecurityInfoResponse\nfrom ..models import db, Device, Command as DBCommand\nfrom commandment.inventory.models import InstalledCertificate, InstalledProfile, InstalledApplication\n\nQueries = DeviceInformation.Queries\n\n\n@command_router.route('DeviceInformation')\ndef ack_device_information(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to the ``DeviceInformation`` command.\n\n    Args:\n        request (Command): An instance of the command that prompted the device to come back with this request.\n        device (Device): The device responding to the command.\n        response (dict): The raw response dictionary, de-serialized from plist.\n    Returns:\n        void: Reserved for future use\n\n    See Also:\n        - `DeviceInformation Command <https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html#//apple_ref/doc/uid/TP40017387-CH3-SW15>`_.\n    \"\"\"\n    schema = DeviceInformationResponse()\n    result = schema.load(response)\n    for k, v in result.data['QueryResponses'].items():\n        setattr(device, k, v)\n\n    db.session.commit()\n\n\n@command_router.route('SecurityInfo')\ndef ack_security_info(request: DBCommand, device: Device, response: dict):\n    schema = SecurityInfoResponse()\n    result = schema.load(response)\n\n\n    db.session.commit()\n\n\n@command_router.route('ProfileList')\ndef ack_profile_list(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a ``ProfileList`` response.\n\n    This is used as the trigger to perform InstallProfile/RemoveProfiles as we have the most current data about\n    what exists on the device.\n\n    The set of profiles to install is a result of:\n\n        set(desired) - set(installed) = set(install)\n\n    The set of profiles to remove is a result of:\n\n        set(installed) - set(desired) = set(remove)\n\n    EXCEPT THAT:\n        - You never want to remove the enrollment profile unless you are \"unmanaging\" the device.\n        - You can't remove profiles not installed by this MDM.\n\n    Args:\n        request (ProfileList): The command instance that generated this response.\n        device (Device): The device responding to the command.\n        response (dict): The raw response dictionary, de-serialized from plist.\n    Returns:\n          void: Reserved for future use\n    \"\"\"\n    schema = ProfileListResponse()\n    profile_list = schema.load(response)\n\n    for pl in device.installed_payloads:\n        db.session.delete(pl)\n\n    # Impossible to calculate delta, so all profiles get wiped\n    for p in device.installed_profiles:\n        db.session.delete(p)\n\n    desired_profiles = {}\n    for tag in device.tags:\n        for p in tag.profiles:\n            desired_profiles[p.uuid] = p\n\n    remove_profiles = []\n\n    for profile in profile_list.data['ProfileList']:\n        profile.device = device\n\n        # device.udid may have dashes (macOS) or not (iOS)\n        profile.device_udid = device.udid\n\n        for payload in profile.payload_content:\n            payload.device = device\n            payload.profile_id = profile.id\n\n        db.session.add(profile)\n\n        # Reconcile profiles which should be installed\n        if profile.payload_uuid in desired_profiles:\n            del desired_profiles[profile.payload_uuid]\n        else:\n            if not profile.is_managed:\n                current_app.logger.debug(\"Skipping removal of unmanaged profile: %s\", profile.payload_display_name)\n            else:\n                current_app.logger.debug(\"Going to remove: %s\", profile.payload_display_name)\n                remove_profiles.append(profile)\n\n    # Queue up some desired profiles\n    for puuid, p in desired_profiles.items():\n        c = commands.InstallProfile(None, profile=p)\n        dbc = DBCommand.from_model(c)\n        dbc.device = device\n        db.session.add(dbc)\n\n    for remove_profile in remove_profiles:\n        c = commands.RemoveProfile(None, Identifier=remove_profile.payload_identifier)\n        dbc = DBCommand.from_model(c)\n        dbc.device = device\n        db.session.add(dbc)\n\n    db.session.commit()\n\n\n@command_router.route('CertificateList')\ndef ack_certificate_list(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to the ``CertificateList`` command.\n\n    Args:\n        request (Command): An instance of the command that prompted the device to come back with this request.\n        device (Device): The database model of the device responding.\n        response (dict): The response plist data, as a dictionary.\n\n    Returns:\n        void: Nothing is returned but this behaviour is subject to change.\n    \"\"\"\n    for c in device.installed_certificates:\n        db.session.delete(c)\n\n    certificates = response['CertificateList']\n    current_app.logger.debug(\n        'Received CertificatesList response containing {} certificate(s)'.format(len(certificates)))\n\n    for cert in certificates:\n        ic = InstalledCertificate()\n        ic.device = device\n        ic.device_udid = device.udid\n\n        ic.x509_cn = cert.get('CommonName', None)\n        ic.is_identity = cert.get('IsIdentity', None)\n\n        der_data = cert['Data']\n        certificate = x509.load_der_x509_certificate(der_data, default_backend())\n        ic.fingerprint_sha256 = hexlify(certificate.fingerprint(hashes.SHA256()))\n        ic.der_data = der_data\n\n        db.session.add(ic)\n\n    db.session.commit()\n\n\n@command_router.route('InstalledApplicationList')\ndef ack_installed_app_list(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to the ``InstalledApplicationList`` command.\n    \n    .. note:: There is no composite key which can uniquely identify an item in the installed applications list.\n        Some applications may not contain any version information at all. For this reason, the entire list of installed\n        applications is cleared before inserting a new list.\n        \n    Args:\n          request (InstalledApplicationList): An instance of the command that generated this response from the managed\n            device.\n          device (Device): The device responding\n          response (dict): The dictionary containing the parsed plist response from the device.\n    Returns:\n          void: Nothing is returned but this behaviour is subject to change.\n    \"\"\"\n\n    for a in device.installed_applications:\n        db.session.delete(a)\n\n    applications = response['InstalledApplicationList']\n    current_app.logger.debug(\n        'Received InstalledApplicationList response containing {} application(s)'.format(len(applications))\n    )\n\n    schema = InstalledApplicationListResponse()\n    result, errors = schema.load(response)\n    current_app.logger.debug(errors)\n    # current_app.logger.info(result)\n\n    ignored_app_bundle_ids = current_app.config['IGNORED_APPLICATION_BUNDLE_IDS']\n\n    for ia in result['InstalledApplicationList']:\n        if isinstance(ia, db.Model):\n            if ia.bundle_identifier in ignored_app_bundle_ids:\n                current_app.logger.debug('Ignoring app with bundle id: %s', ia.bundle_identifier)\n                continue\n\n            ia.device = device\n            ia.device_udid = device.udid\n            db.session.add(ia)\n        else:\n            current_app.logger.debug('Not a model: %s', ia)\n\n    db.session.commit()\n\n\n@command_router.route('InstallProfile')\ndef ack_install_profile(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to ``InstallProfile``.\"\"\"\n    if response.get('Status', None) == 'Error':\n        pass\n\n\n@command_router.route('RemoveProfile')\ndef ack_install_profile(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to ``RemoveProfile``.\"\"\"\n    if response.get('Status', None) == 'Error':\n        pass\n\n\n@command_router.route('AvailableOSUpdates')\ndef ack_available_os_updates(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to AvailableOSUpdates\"\"\"\n    if response.get('Status', None) == 'Error':\n        pass\n    else:\n        for au in device.available_os_updates:\n            db.session.delete(au)\n\n        schema = AvailableOSUpdateListResponse()\n        result = schema.load(response)\n\n        for upd in result.data['AvailableOSUpdates']:\n            upd.device = device\n            db.session.add(upd)\n\n        db.session.commit()\n\n\n@command_router.route('InstallApplication')\ndef ack_install_application(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to InstallApplication.\n\n    We will insert this into `managed_applications` to show that there is a pending application install.\n    `managed_applications` will be the source of truth for installation status.\n\n    If the result of `InstallApplication` is a user prompt, we cannot send further IA commands until the prompt has\n    been resolved(?) as seen on iOS 11.3.1\n\n    TODO: Also create a pending status when the command is queued but not acked\n    \"\"\"\n    if response.get('Status', None) == 'Error':\n        pass\n    else:\n        try:\n            # It is possible to send `InstallApplication` and receive Acknowledged multiple times for the same app,\n            # so we want to avoid multiple rows in that scenario\n            ma = db.session.query(ManagedApplication).filter(\n                Device.id == device.id,\n                ManagedApplication.bundle_id == response['Identifier']\n            ).one()\n            ma.ia_command = request\n            db.session.commit()\n\n        except NoResultFound:\n            ma = ManagedApplication()\n            ma.device = device\n            ma.bundle_id = response['Identifier']\n            ma.status = ManagedAppStatus(response['State'])\n            ma.ia_command = request\n\n            db.session.add(ma)\n            db.session.commit()\n\n\n@command_router.route('ManagedApplicationList')\ndef ack_managed_application_list(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to `ManagedApplicationList`.\"\"\"\n    if response.get('Status', None) == 'Error':\n        pass\n    else:\n        for bundle_id, status in response['ManagedApplicationList'].items():\n            try:\n                ma = db.session.query(ManagedApplication).filter(\n                    Device.id == device.id,\n                    ManagedApplication.bundle_id == bundle_id\n                ).one()\n            except NoResultFound:\n                ma = ManagedApplication(bundle_id=bundle_id, device=device)\n\n            ma.status = ManagedAppStatus(status['Status'])\n            ma.external_version_id = status.get('ExternalVersionIdentifier', None)  # Does not exist in iOS 11.3.1\n            ma.has_configuration = status['HasConfiguration']\n            ma.has_feedback = status['HasFeedback']\n            ma.is_validated = status['IsValidated']\n            ma.management_flags = status['ManagementFlags']\n\n            db.session.add(ma)\n\n        db.session.commit()\n\n        for tag in device.tags:\n            for app in tag.applications:\n                # TODO: need to check with new versions being available. This is very primitive.\n                if app.bundle_id in response['ManagedApplicationList'].keys():\n                    continue\n\n                c = commands.InstallApplication(application=app)\n                dbc = DBCommand.from_model(c)\n                dbc.device = device\n                db.session.add(dbc)\n\n                ma = ManagedApplication(device=device, application=app, ia_command=dbc, status=ManagedAppStatus.Queued)\n                db.session.add(ma)\n\n        db.session.commit()\n\n\n@command_router.route('RestartDevice')\ndef ack_restart_device(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to `RestartDevice`.\n\n    On macOS 10.13.6, the MDM client comes back with an `Idle` check in upon restart as part of launchd starting up.\n    This happens BEFORE the loginwindow (at about 40% of the progress bar at startup). This is the same Power-on\n    behaviour.\n    \"\"\"\n    pass\n\n\n@command_router.route('ShutDownDevice')\ndef ack_restart_device(request: DBCommand, device: Device, response: dict):\n    \"\"\"Acknowledge a response to `ShutDownDevice`.\n\n    On macOS 10.13.6, the MDM client comes back with an `Idle` check in upon restart as part of launchd starting up.\n    This happens BEFORE the loginwindow (at about 40% of the progress bar at startup). This is the same Power-on\n    behaviour.\n    \"\"\"\n    pass\n"
  },
  {
    "path": "commandment/mdm/models.py",
    "content": ""
  },
  {
    "path": "commandment/mdm/resources.py",
    "content": "from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship\nfrom flask_rest_jsonapi.exceptions import ObjectNotFound\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom commandment.mdm.schema import CommandSchema\nfrom commandment.models import db, Command, Device\n\n\nclass CommandsList(ResourceList):\n    def query(self, view_kwargs):\n        query_ = self.session.query(Command)\n        if view_kwargs.get('device_id') is not None:\n            try:\n                self.session.query(Device).filter_by(id=view_kwargs['device_id']).one()\n            except NoResultFound:\n                raise ObjectNotFound({'parameter': 'device_id'}, \"Device: {} not found\".format(view_kwargs['device_id']))\n            else:\n                query_ = query_.join(Device).filter(Device.id == view_kwargs['device_id'])\n        return query_\n\n    schema = CommandSchema\n    view_kwargs = True\n    data_layer = {\n        'session': db.session,\n        'model': Command,\n        'methods': {'query': query}\n    }\n\n\nclass CommandDetail(ResourceDetail):\n    schema = CommandSchema\n    data_layer = {\n        'session': db.session,\n        'model': Command,\n        'url_field': 'command_id'\n    }\n\n\nclass CommandRelationship(ResourceRelationship):\n    schema = CommandSchema\n    data_layer = {'session': db.session, 'model': Command}\n"
  },
  {
    "path": "commandment/mdm/response_schema.py",
    "content": "from typing import Optional\nfrom marshmallow import Schema, fields, post_load, ValidationError\nfrom marshmallow_enum import EnumField\nfrom enum import IntFlag\n\nimport commandment.inventory.models\nfrom .. import models\nfrom commandment.inventory import models as inventory_models\n\n\nclass ErrorChainItem(Schema):\n    \"\"\"ErrorChainItem describes an item of the ErrorChain array,\n     which appears when an error occurs with an MDM request.\"\"\"\n    LocalizedDescription = fields.String()\n    USEnglishDescription = fields.String()\n    ErrorDomain = fields.String()\n    ErrorCode = fields.Number()\n\n\nclass CommandResponse(Schema):\n    \"\"\"CommandResponse is the base class for all MDM Response Schemas.\"\"\"\n    Status = fields.String()\n    UDID = fields.String()\n    CommandUUID = fields.UUID()\n    ErrorChain = fields.Nested(ErrorChainItem, many=True)\n\n\nclass OrganizationInfo(Schema):\n    pass\n\n\nclass AutoSetupAdminAccount(Schema):\n    GUID = fields.UUID()\n    shortName = fields.String()\n\n\nclass OSUpdateSettings(Schema):\n    \"\"\"OSUpdateSettings is returned as a nested part of the ``DeviceInformation`` response.\"\"\"\n    CatalogURL = fields.String(attribute='osu_catalog_url')\n    IsDefaultCatalog = fields.Boolean(attribute='osu_is_default_catalog')\n    PreviousScanDate = fields.Date(attribute='osu_previous_scan_date')\n    PreviousScanResult = fields.String(attribute='osu_previous_scan_result')\n    PerformPeriodicCheck = fields.Boolean(attribute='osu_perform_periodic_check')\n    AutomaticCheckEnabled = fields.Boolean(attribute='osu_automatic_check_enabled')\n    BackgroundDownloadEnabled = fields.Boolean(attribute='osu_background_download_enabled')\n    AutomaticAppInstallationEnabled = fields.Boolean(attribute='osu_automatic_app_installation_enabled')\n    AutomaticOSInstallationEnabled = fields.Boolean(attribute='osu_automatic_os_installation_enabled')\n    AutomaticSecurityUpdatesEnabled = fields.Boolean(attribute='osu_automatic_security_updates_enabled')\n\n\nclass DeviceInformation(Schema):\n    # Table 5\n    UDID = fields.String(attribute='udid')\n    # Languages\n    DeviceID = fields.String(attribute='device_id')\n    OrganizationInfo = fields.Nested(OrganizationInfo)\n    LastCloudBackupDate = fields.Date(attribute='last_cloud_backup_date')\n    AwaitingConfiguration = fields.Boolean(attribute='awaiting_configuration')\n    AutoSetupAdminAccounts = fields.Nested(AutoSetupAdminAccount, many=True)\n\n    # Table 6\n    iTunesStoreAccountIsActive = fields.Boolean(attribute='itunes_store_account_is_active')\n    iTunesStoreAccountHash = fields.String(attribute='itunes_store_account_hash')\n\n    # Table 7\n    DeviceName = fields.String(attribute='device_name')\n    OSVersion = fields.String(attribute='os_version')\n    BuildVersion = fields.String(attribute='build_version')\n    ModelName = fields.String(attribute='model_name')\n    Model = fields.String(attribute='model')\n    ProductName = fields.String(attribute='product_name')\n    SerialNumber = fields.String(attribute='serial_number')\n    DeviceCapacity = fields.Float(attribute='device_capacity')\n    AvailableDeviceCapacity = fields.Float(attribute='available_device_capacity')\n    BatteryLevel = fields.Float(attribute='battery_level')\n    CellularTechnology = fields.Integer(attribute='cellular_technology')\n    IMEI = fields.String(attribute='imei')\n    MEID = fields.String(attribute='meid')\n    ModemFirmwareVersion = fields.String(attribute='modem_firmware_version')\n    IsSupervised = fields.Boolean(attribute='is_supervised')\n    IsDeviceLocatorServiceEnabled = fields.Boolean(attribute='is_device_locator_service_enabled')\n    IsActivationLockEnabled = fields.Boolean(attribute='is_activation_lock_enabled')\n    IsDoNotDisturbInEffect = fields.Boolean(attribute='is_do_not_disturb_in_effect')\n    EASDeviceIdentifier = fields.String(attribute='eas_device_identifier')\n    IsCloudBackupEnabled = fields.Boolean(attribute='is_cloud_backup_enabled')\n    OSUpdateSettings = fields.Nested(OSUpdateSettings, attribute='os_update_settings')  # T8\n    LocalHostName = fields.String(attribute='local_hostname')\n    HostName = fields.String(attribute='hostname')\n    SystemIntegrityProtectionEnabled = fields.Boolean(attribute='sip_enabled')\n    # Array of str\n    #ActiveManagedUsers = fields.Nested(ActiveManagedUser)\n    IsMDMLostModeEnabled = fields.Boolean(attribute='is_mdm_lost_mode_enabled')\n    MaximumResidentUsers = fields.Integer(attribute='maximum_resident_users')\n\n    # Table 9\n    ICCID = fields.String(attribute='iccid')\n    BluetoothMAC = fields.String(attribute='bluetooth_mac')\n    WiFiMAC = fields.String(attribute='wifi_mac')\n    EthernetMACs = fields.String(attribute='ethernet_macs', many=True)\n    CurrentCarrierNetwork = fields.String(attribute='current_carrier_network')\n    SIMCarrierNetwork = fields.String(attribute='sim_carrier_network')\n    SubscriberCarrierNetwork = fields.String(attribute='subscriber_carrier_network')\n    CarrierSettingsVersion = fields.String(attribute='carrier_settings_version')\n    PhoneNumber = fields.String(attribute='phone_number')\n    VoiceRoamingEnabled = fields.Boolean(attribute='voice_roaming_enabled')\n    DataRoamingEnabled = fields.Boolean(attribute='data_roaming_enabled')\n    IsRoaming = fields.Boolean(attribute='is_roaming')\n    PersonalHotspotEnabled = fields.Boolean(attribute='personal_hotspot_enabled')\n    SubscriberMCC = fields.String(attribute='subscriber_mcc')\n    SubscriberMNC = fields.String(attribute='subscriber_mnc')\n    CurrentMCC = fields.String(attribute='current_mcc')\n    CurrentMNC = fields.String(attribute='current_mnc')\n\n    @post_load\n    def normalize_osu(self, data):\n        print(data)\n        for k, v in data.get('os_update_settings', {}).items():\n            setattr(data, k, v)\n        return data\n\n\nclass DeviceInformationResponse(CommandResponse):\n    QueryResponses = fields.Nested(DeviceInformation)\n\n\nclass InstallApplicationResponse(CommandResponse):\n    Identifier = fields.String()\n    State = fields.String()\n\n\nclass HardwareEncryptionCaps(IntFlag):\n    Nothing = 0\n    BlockLevelEncryption = 1\n    FileLevelEncryption = 2\n\n    All = BlockLevelEncryption | FileLevelEncryption\n\n\nclass FirewallApplicationItem(Schema):\n    BundleID = fields.String()\n    Allowed = fields.Boolean()\n    Name = fields.String()\n\n\nclass FirewallSettings(Schema):\n    FirewallEnabled = fields.Boolean()\n    BlockAllIncoming = fields.Boolean()\n    StealthMode = fields.Boolean()\n    Applications = fields.Nested(FirewallApplicationItem, many=True)\n\n\nclass FirmwarePasswordStatus(Schema):\n    PasswordExists = fields.Boolean()\n    ChangePending = fields.Boolean()\n    AllowOroms = fields.Boolean()\n\n\nclass ManagementStatus(Schema):\n    EnrolledViaDEP = fields.Boolean()\n    UserApprovedEnrollment = fields.Boolean()\n\n\nclass SecurityInfoResponse(CommandResponse):\n    HardwareEncryptionCaps = EnumField(HardwareEncryptionCaps)\n    PasscodePresent = fields.Boolean()\n    PasscodeCompliant = fields.Boolean()\n    PasscodeCompliantWithProfiles = fields.Boolean()\n    PasscodeLockGracePeriodEnforced = fields.Integer()\n    FDE_Enabled = fields.Boolean()\n    FDE_HasPersonalRecoveryKey = fields.Boolean()\n    FDE_HasInstitutionalRecoveryKey = fields.Boolean()\n    FDE_PersonalRecoveryKeyCMS = fields.String()\n    FDE_PersonalRecoveryKeyDeviceKey = fields.String()\n    FirewallSettings = fields.Nested(FirewallSettings)\n    SystemIntegrityProtectionEnabled = fields.Boolean()\n    FirmwarePasswordStatus = fields.Nested(FirmwarePasswordStatus)\n    ManagementStatus = fields.Nested(ManagementStatus)\n\n\nclass InstalledApplicationItem(Schema):\n    AdHocCodeSigned = fields.Boolean(attribute='adhoc_codesigned')\n    AppStoreVendable = fields.Boolean(attribute='appstore_vendable')\n    BetaApp = fields.Boolean(attribute='beta_app')\n    DeviceBasedVPP = fields.Boolean(attribute='device_based_vpp')\n    HasUpdateAvailable = fields.Boolean(attribute='has_update_available')\n    Installing = fields.Boolean(attribute='installing')\n    Identifier = fields.String(attribute='bundle_identifier')\n    Version = fields.String(attribute='version')\n    ShortVersion = fields.String(attribute='short_version')\n    Name = fields.String(attribute='name')\n    BundleSize = fields.Integer(attribute='bundle_size')\n    DynamicSize = fields.Integer(attribute='dynamic_size')\n    IsValidated = fields.Boolean(attribute='is_validated')\n    ExternalVersionIdentifier = fields.Integer(attribute='external_version_identifier')  # iOS 11\n\n    @post_load(pass_many=False)\n    def make_installed_application(self, data: Optional[dict]) -> Optional[inventory_models.InstalledApplication]:\n        return inventory_models.InstalledApplication(**data)\n\n\nclass InstalledApplicationListResponse(CommandResponse):\n    InstalledApplicationList = fields.Nested(InstalledApplicationItem, many=True)\n\n\nclass CertificateListItem(Schema):\n    CommonName = fields.String()\n    IsIdentity = fields.Boolean()\n    Data = fields.String()\n\n    @post_load\n    def make_installed_certificate(self, data: dict) -> inventory_models.InstalledCertificate:\n        return inventory_models.InstalledCertificate(**data)\n\n\nclass CertificateListResponse(CommandResponse):\n    CertificateList = fields.Nested(CertificateListItem, many=True)\n\n\nclass AvailableOSUpdate(Schema):\n    AllowsInstallLater = fields.Boolean(attribute='allows_install_later')\n    Build = fields.String(attribute='build')\n    DownloadSize = fields.Number(attribute='download_size')\n    AppIdentifiersToClose = fields.List(fields.String, attribute='app_identifiers_to_close', many=True)\n    HumanReadableName = fields.String(attribute='human_readable_name')\n    HumanReadableNameLocale = fields.String(attribute='human_readable_name_locale')\n    InstallSize = fields.Number(attribute='install_size')\n    IsConfigDataUpdate = fields.Boolean(attribute='is_config_data_update')\n    IsCritical = fields.Boolean(attribute='is_critical')\n    IsFirmwareUpdate = fields.Boolean(attribute='is_firmware_update')\n    MetadataURL = fields.String(attribute='metadata_url')\n    ProductKey = fields.String(attribute='product_key')\n    ProductName = fields.String(attribute='product_name')\n    RestartRequired = fields.Boolean(attribute='restart_required')\n    Version = fields.String(attribute='version')\n\n    @post_load\n    def make_available_os_update(self, data: dict) -> commandment.inventory.models.AvailableOSUpdate:\n        return commandment.inventory.models.AvailableOSUpdate(**data)\n\n\nclass AvailableOSUpdateListResponse(CommandResponse):\n    AvailableOSUpdates = fields.Nested(AvailableOSUpdate, many=True)\n\n\nclass ProfileListPayloadItem(Schema):\n    PayloadDescription = fields.String(attribute='description')\n    PayloadDisplayName = fields.String(attribute='display_name')\n    PayloadIdentifier = fields.String(attribute='identifier')\n    PayloadOrganization = fields.String(attribute='organization')\n    PayloadType = fields.String(attribute='payload_type')\n    PayloadUUID = fields.UUID(attribute='uuid')\n    # PayloadVersion = fields.Integer(attribute='payload_version')\n\n    @post_load\n    def make_installed_payload(self, data: dict) -> inventory_models.InstalledPayload:\n        return inventory_models.InstalledPayload(**data)\n\n\nclass ProfileListItem(Schema):\n    HasRemovalPasscode = fields.Boolean(attribute='has_removal_password')\n    IsEncrypted = fields.Boolean(attribute='is_encrypted')\n    IsManaged = fields.Boolean(attribute='is_managed')\n    PayloadDescription = fields.String(attribute='payload_description')\n    PayloadDisplayName = fields.String(attribute='payload_display_name')\n    PayloadIdentifier = fields.String(attribute='payload_identifier')\n    PayloadOrganization = fields.String(attribute='payload_organization')\n    PayloadRemovalDisallowed = fields.Boolean(attribute='payload_removal_disallowed')\n    PayloadUUID = fields.UUID(attribute='payload_uuid')\n    # PayloadVersion = fields.Integer(attribute='payload_version')\n    #SignerCertificates = fields.Nested(attribute='signer_certificates', many=True)\n    PayloadContent = fields.Nested(ProfileListPayloadItem, attribute='payload_content', many=True)\n\n    @post_load\n    def make_installed_profile(self, data: dict) -> inventory_models.InstalledProfile:\n        return inventory_models.InstalledProfile(**data)\n\n\nclass ProfileListResponse(CommandResponse):\n    ProfileList = fields.Nested(ProfileListItem, many=True)\n\n"
  },
  {
    "path": "commandment/mdm/routers.py",
    "content": "\"\"\"This module contains routers which direct the request towards a certain module or function based upon the CONTENT\nof the request, rather than the URL.\"\"\"\n\nfrom typing import Union, Any, Type, Callable, Dict, List\nfrom flask import Flask, app, Blueprint, request, abort, current_app\nfrom functools import wraps\nimport biplist\nfrom commandment.models import db, Device, Command\nfrom commandment.mdm import commands\n\nCommandHandler = Callable[[Command, Device, dict], None]\nCommandHandlers = Dict[str, CommandHandler]\n\n\nclass CommandRouter(object):\n    \"\"\"The command router passes off commands to handlers which are registered by RequestType.\n    \n    When a reply is received from a device in relation to a specific CommandUUID, the router attempts to find a handler\n     that was registered for the RequestType associated with that command. The handler is then called with the specific\n     instance of the command that generated the response, and an instance of the device that is making the request to\n     the MDM endpoint.\n\n    Not handling the error status here allows handlers to freely interpret the error conditions of each response, which\n    is generally a better approach as some errors are command specific.\n    \n    Args:\n          app (app): The flask application or blueprint instance\n    \"\"\"\n    def __init__(self, app: Union[Flask, Blueprint]) -> None:\n        self._app = app\n        self._handlers: CommandHandlers = {}\n\n    def handle(self, command: Command, device: Device, response: dict):\n        current_app.logger.debug('Looking for handler using command: {}'.format(command.request_type))\n        if command.request_type in self._handlers:\n            return self._handlers[command.request_type](command, device, response)\n        else:\n            current_app.logger.warning('No handler found to process command response: {}'.format(command.request_type))\n            return None\n\n    def route(self, request_type: str):\n        \"\"\"\n        Route a plist request by its RequestType key value.\n        \n        The wrapped function must accept (command, plist_data)\n        \n        :param request_type: \n        :return: \n        \"\"\"\n        handlers = self._handlers\n        # current_app.logger.debug('Registering command handler for request type: {}'.format(request_type))\n\n        def decorator(f):\n            handlers[request_type] = f\n\n            @wraps(f)\n            def wrapped(*args, **kwargs):\n                return f(*args, **kwargs)\n\n            return wrapped\n        return decorator\n\n\nclass PlistRouter(object):\n    \"\"\"PlistRouter routes requests to view functions based on matching values to top level keys.\n    \n    \"\"\"\n    def __init__(self, app: app, url: str) -> None:\n        self._app = app\n        app.add_url_rule(url, view_func=self.view, methods=['PUT'])\n        self.kv_routes: List[Dict[str, Any]] = []\n\n    def view(self):\n        current_app.logger.debug(request.data)\n\n        try:\n            plist_data = biplist.readPlistFromString(request.data)\n        except biplist.NotBinaryPlistException:\n            abort(400, 'The request body does not contain a plist as expected')\n        except biplist.InvalidPlistException:\n            abort(400, 'The request body does not contain a valid plist')\n\n        for kvr in self.kv_routes:\n            if kvr['key'] not in plist_data:\n                continue\n\n            if plist_data[kvr['key']] == kvr['value']:\n                return kvr['handler'](plist_data)\n\n        abort(404, 'No matching plist route')\n\n    def route(self, key: str, value: Any):\n        \"\"\"\n        Route a plist request if the content satisfies the key value test\n        \n        The wrapped function must accept (plist_data)\n        \"\"\"\n        def decorator(f):\n            self.kv_routes.append(dict(\n                key=key,\n                value=value,\n                handler=f\n            ))\n\n            @wraps(f)\n            def wrapped(*args, **kwargs):\n                return f(*args, **kwargs)\n\n            return wrapped\n        return decorator\n"
  },
  {
    "path": "commandment/mdm/schema.py",
    "content": "from marshmallow_jsonapi import fields\nfrom marshmallow_jsonapi.flask import Relationship, Schema\n\n\nclass CommandSchema(Schema):\n    class Meta:\n        type_ = 'commands'\n        self_view = 'api_app.command_detail'\n        self_view_kwargs = {'command_id': '<id>'}\n        self_view_many = 'api_app.commands_list'\n        strict = True\n\n    id = fields.Int(dump_only=True)\n    uuid = fields.Str(dump_only=True)\n    request_type = fields.Str()\n    status = fields.Str()\n    queued_at = fields.DateTime()\n    sent_at = fields.DateTime()\n    acknowledged_at = fields.DateTime()\n    after = fields.DateTime()\n    ttl = fields.Int()\n\n    device = Relationship(\n        related_view='api_app.device_detail',\n        related_view_kwargs={'device_id': '<id>'},\n        type_='devices'\n    )\n\n"
  },
  {
    "path": "commandment/mdm/util.py",
    "content": "from commandment.mdm import commands\nfrom commandment.models import db, Device, Command\n\n\ndef queryresponses_to_query_set(responses: dict):\n    return {commands.DeviceInformation.Queries(k): v for k, v in responses.items()}\n\n\ndef queue_full_inventory(device: Device):\n    \"\"\"Enqueue all inventory commands for a device.\n\n    Typically run at first check-in\n\n    Args:\n          device (Device): The device\n    \"\"\"\n    # DeviceInformation\n    di = commands.DeviceInformation.for_platform(device.platform, device.os_version)\n    db_command = Command.from_model(di)\n    db_command.device = device\n    db.session.add(db_command)\n\n    # InstalledApplicationList - Pretty taxing so don't run often\n    ial = commands.InstalledApplicationList()\n    db_command_ial = Command.from_model(ial)\n    db_command_ial.device = device\n    db.session.add(db_command_ial)\n\n    # CertificateList\n    cl = commands.CertificateList()\n    dbc = Command.from_model(cl)\n    dbc.device = device\n    db.session.add(dbc)\n\n    # SecurityInfo\n    si = commands.SecurityInfo()\n    dbsi = Command.from_model(si)\n    dbsi.device = device\n    db.session.add(dbsi)\n\n    # ProfileList\n    pl = commands.ProfileList()\n    db_pl = Command.from_model(pl)\n    db_pl.device = device\n    db.session.add(db_pl)\n\n    # AvailableOSUpdates\n    au = commands.AvailableOSUpdates()\n    au_pl = Command.from_model(au)\n    au_pl.device = device\n    db.session.add(au_pl)\n\n    db.session.commit()\n"
  },
  {
    "path": "commandment/models.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2017 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\nAttributes:\n    db (SQLAlchemy): A reference to flask SQLAlchemy extensions db instance.\n\"\"\"\nfrom typing import Optional, Type\nfrom flask_sqlalchemy import SQLAlchemy\n\nimport datetime\nfrom enum import Enum, IntEnum\nfrom sqlalchemy.ext.mutable import MutableDict\nfrom sqlalchemy.ext.hybrid import hybrid_property\n\nfrom .dbtypes import GUID, JSONEncodedDict\nfrom .mdm import CommandStatus, Platform, commands\nimport base64\nfrom binascii import hexlify\nfrom biplist import Data as NSData\nfrom .profiles.certificates import KeyUsage\n\ndb = SQLAlchemy()\n\n\nclass CellularTechnology(IntEnum):\n    Nothing = 0\n    GSM = 1\n    CDMA = 2\n    Both = 3\n\n\ndevice_tags = db.Table(\n    'device_tags',\n    db.metadata,\n    db.Column('device_id', db.Integer, db.ForeignKey('devices.id')),\n    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')),\n)\n\n\nclass Device(db.Model):\n    \"\"\"An enrolled device.\n    \n    :table: devices\n    \"\"\"\n    __tablename__ = 'devices'\n\n    # Common attributes\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (int):\"\"\"\n    udid = db.Column(db.String(40), index=True, nullable=True)\n    \"\"\"udid (str): Unique Device Identifier\"\"\"\n    last_seen = db.Column(db.DateTime, nullable=True)\n    \"\"\"last_seen (datetime.datetime): When the device last contacted the MDM.\"\"\"\n    is_enrolled = db.Column(db.Boolean, default=False)\n    \"\"\"is_enrolled (bool): Whether the MDM should consider this device enrolled.\"\"\"\n\n    # APNS / Push\n    topic = db.Column(db.String, nullable=True)\n    \"\"\"topic (str): The APNS topic the device is listening on.\"\"\"\n    push_magic = db.Column(db.String, nullable=True)\n    \"\"\"push_magic (str): The UUID that establishes a unique relationship between the device and the MDM.\"\"\"\n    # The APNS device token is stored in base64 format. Descriptors are added to handle this encoding and decoding\n    # to bytes automatically.\n    _token = db.Column(db.String, nullable=True)\n    tokenupdate_at = db.Column(db.DateTime)\n    # if null there are no outstanding push notifications. If this contains anything then dont attempt to deliver\n    # another APNS push.\n    last_push_at = db.Column(db.DateTime, nullable=True)\n    \"\"\"last_push_at (datetime.datetime): The datetime when the last push was sent to APNS for this device.\"\"\"\n    last_apns_id = db.Column(db.Integer, nullable=True)\n    \"\"\"last_apns_id (str): The UUID of the last apns command sent.\"\"\"\n    # if the time delta between last_push_at and last_seen is >= several days to a week,\n    # this should count as a failed push, and potentially declare the device as dead.\n    failed_push_count = db.Column(db.Integer, default=0, nullable=False)\n\n    # Table 5\n    last_cloud_backup_date = db.Column(db.DateTime)\n    \"\"\"last_cloud_backup_date (datetime): The date of the last iCloud backup.\"\"\"\n    awaiting_configuration = db.Column(db.Boolean)\n    \"\"\"awaiting_configuration (bool): True if device is waiting at Setup Assistant\"\"\"\n\n    # Table 6\n    itunes_store_account_is_active = db.Column(db.Boolean)\n    \"\"\"itunes_store_account_is_active (bool): the user is currently logged into an active iTunes Store account.\"\"\"\n    itunes_store_account_hash = db.Column(db.String)\n    \"\"\"itunes_store_account_hash (str): a hash of the iTunes Store account currently logged in.\"\"\"\n\n    # DeviceInformation : Table 7\n    device_name = db.Column(db.String)  # Authenticate\n    \"\"\"device_name (str): Name of the device\"\"\"\n    os_version = db.Column(db.String)  # Authenticate\n    \"\"\"os_version (str): The operating system version number.\"\"\"\n    build_version = db.Column(db.String)  # Authenticate\n    \"\"\"build_version (str): DeviceInformation BuildVersion\"\"\"\n    model_name = db.Column(db.String)  # Authenticate\n    \"\"\"model_name (str): Longer name of the hardware model\"\"\"\n    model = db.Column(db.String)  # Authenticate\n    \"\"\"model (str): Name of the hardware model\"\"\"\n    product_name = db.Column(db.String)  # Authenticate\n    \"\"\"product_name (str): The base product name of the hardware\"\"\"\n    serial_number = db.Column(db.String(64), index=True, nullable=True)  # Authenticate\n    \"\"\"serial_number (str): The hardware serial number\"\"\"\n    device_capacity = db.Column(db.Float, nullable=True)\n    \"\"\"device_capacity (float): total capacity (base 1024 gigabytes)\"\"\"\n    available_device_capacity = db.Column(db.Float, nullable=True)\n    \"\"\"device_available_capacity (float): available capacity (base 1024 gigabytes)\"\"\"\n    battery_level = db.Column(db.Float, default=-1.0)\n    \"\"\"battery_level (float): battery level, between 0.0 and 1.0. -1.0 if information is not available.\"\"\"\n    cellular_technology = db.Column(db.Enum(CellularTechnology))\n    \"\"\"cellular_technology (CellularTechnology): cellular technology.\"\"\"\n    imei = db.Column(db.String)\n    \"\"\"imei (str): IMEI number (if device is GSM).\"\"\"\n    meid = db.Column(db.String)\n    \"\"\"meid (str): MEID number (if device is CSMA).\"\"\"\n    modem_firmware_version = db.Column(db.String)\n    \"\"\"modem_firmware_version (str): The baseband firmware version.\"\"\"\n    is_supervised = db.Column(db.Boolean)\n    \"\"\"is_supervised (bool): Device is supervised\"\"\"\n    is_device_locator_service_enabled = db.Column(db.Boolean)\n    \"\"\"is_device_locator_service_enabled (bool): Find My iPhone/Mac enabled.\"\"\"\n    is_activation_lock_enabled = db.Column(db.Boolean)\n    \"\"\"is_activation_lock_enabled (bool): Device has Activation Lock enabled.\"\"\"\n    is_do_not_disturb_in_effect = db.Column(db.Boolean)\n    \"\"\"is_do_not_disturb_in_effect (bool): Device has DND enabled.\"\"\"\n    device_id = db.Column(db.String)  # ATV\n    \"\"\"device_id (str): Device ID (ATV)\"\"\"\n    eas_device_identifier = db.Column(db.String)\n    \"\"\"eas_device_identifier (str): Exchange ActiveSync Identifier\"\"\"\n    is_cloud_backup_enabled = db.Column(db.Boolean)\n    \"\"\"is_cloud_backup_enabled (bool): iCloud backup is enabled.\"\"\"\n    local_hostname = db.Column(db.String)\n    \"\"\"local_hostname (str): \"\"\"\n    hostname = db.Column(db.String)\n    \"\"\"hostname (str): \"\"\"\n    sip_enabled = db.Column(db.Boolean)\n    \"\"\"sip_enabled (bool): System Integrity Protection is enabled.\"\"\"\n    # TODO: ActiveManagedUsers\n    is_mdm_lost_mode_enabled = db.Column(db.Boolean)\n    \"\"\"is_mdm_lost_mode_enabled (bool): MDM Lost mode is enabled.\"\"\"\n    maximum_resident_users = db.Column(db.Integer)\n    \"\"\"maximum_resident_users (int): Maximum number of users that can use Shared iPad.\"\"\"\n\n    # OSUpdateSettings : Table 8\n    # OSUpdateSettings is flattened\n    osu_catalog_url = db.Column(db.String)\n    \"\"\"osu_catalog_url (str): Software Update Catalog URL.\"\"\"\n    osu_is_default_catalog = db.Column(db.Boolean)\n    osu_previous_scan_date = db.Column(db.DateTime)\n    osu_previous_scan_result = db.Column(db.String)\n    osu_perform_periodic_check = db.Column(db.Boolean)\n    osu_automatic_check_enabled = db.Column(db.Boolean)\n    osu_background_download_enabled = db.Column(db.Boolean)\n    osu_automatic_app_installation_enabled = db.Column(db.Boolean)\n    osu_automatic_os_installation_enabled = db.Column(db.Boolean)\n    osu_automatic_security_updates_enabled = db.Column(db.Boolean)\n\n    # NetworkInfo : Table 9\n    iccid = db.Column(db.String)\n    \"\"\"iccid (str): The ICC identifier for the SIM card.\"\"\"\n    bluetooth_mac = db.Column(db.String)\n    \"\"\"bluetooth_mac (str): The bluetooth MAC address\"\"\"\n    wifi_mac = db.Column(db.String)\n    \"\"\"wifi_mac (str): The WiFi MAC address\"\"\"\n    # TODO: EthernetMACs\n    current_carrier_network = db.Column(db.String)\n    \"\"\"current_carrier_network (str): Name of the current carrier network.\"\"\"\n    sim_carrier_network = db.Column(db.String)\n    \"\"\"sim_carrier_network (str): Name of the home carrier network.\"\"\"\n    subscriber_carrier_network = db.Column(db.String)\n    \"\"\"subscriber_carrier_network (str): Name of the home carrier network (replaces sim_carrier_network).\"\"\"\n    carrier_settings_version = db.Column(db.String)\n    \"\"\"carrier_settings_version (str): Version of the current carrier settings file.\"\"\"\n    phone_number = db.Column(db.String)\n    \"\"\"phone_number (str): Raw phone number without punctuation.\"\"\"\n    voice_roaming_enabled = db.Column(db.Boolean)\n    \"\"\"voice_roaming_enabled (bool): Voice Roaming is enabled in settings.\"\"\"\n    data_roaming_enabled = db.Column(db.Boolean)\n    \"\"\"data_roaming_enabled (bool): Data Roaming is enabled in settings.\"\"\"\n    is_roaming = db.Column(db.Boolean)\n    \"\"\"is_roaming (bool): The device is currently roaming.\"\"\"\n    personal_hotspot_enabled = db.Column(db.Boolean)\n    \"\"\"personal_hotspot_enabled (bool): Personal HotSpot is currently turned on.\"\"\"\n    subscriber_mcc = db.Column(db.String)\n    \"\"\"subscriber_mcc (str): Home Mobile Country Code (numeric)\"\"\"\n    subscriber_mnc = db.Column(db.String)\n    \"\"\"subscriber_mnc (str): Home Mobile Network Code (numeric)\"\"\"\n    current_mcc = db.Column(db.String)\n    \"\"\"current_mcc (str): Current Mobile Country Code (numeric)\"\"\"\n    current_mnc = db.Column(db.String)\n    \"\"\"current_mnc (str): Current Mobile Network Code (numeric)\"\"\"\n\n    # SecurityInfo\n    # hardware_encryption_caps = db.Column(DBEnum(HardwareEncryptionCaps))\n    passcode_present = db.Column(db.Boolean)\n    \"\"\"passcode_present (bool): Device has a passcode.\"\"\"\n    passcode_compliant = db.Column(db.Boolean)\n    \"\"\"passcode_compliant (bool): The passcode is compliant with all requirements (incl Exchange accounts).\"\"\"\n    passcode_compliant_with_profiles = db.Column(db.Boolean)\n    \"\"\"passcode_compliant_with_profiles (bool): The passcode is compliant with profile requirements.\"\"\"\n    passcode_lock_grace_period_enforced = db.Column(db.Integer)\n    \"\"\"passcode_lock_grace_period_enforced (int): The current enforced time in seconds before unlock passcode will \n    be required.\"\"\"\n    fde_enabled = db.Column(db.Boolean)\n    \"\"\"fde_enabled (bool): Whether full disk encryption is enabled or not.\"\"\"\n    fde_has_prk = db.Column(db.Boolean)\n    \"\"\"fde_has_prk (bool): Whether FDE has a personal recovery key set.\"\"\"\n    fde_has_irk = db.Column(db.Boolean)\n    \"\"\"fde_has_irk (bool): Whether FDE has an institutional recovery key set.\"\"\"\n    fde_personal_recovery_key_cms = db.Column(db.LargeBinary)  # 10.13\n    \"\"\"fde_personal_recovery_key_cms (bytes): If Escrow is enabled, contains the encrypted PRK\"\"\"\n    fde_personal_recovery_key_device_key = db.Column(db.String)  # 10.13\n    \"\"\"fde_personal_recovery_key_device_key (str):\"\"\"\n    firewall_enabled = db.Column(db.Boolean)\n    \"\"\"firewall_enabled (bool): Application firewall is enabled.\"\"\"\n    block_all_incoming = db.Column(db.Boolean)\n    \"\"\"block_all_incoming (bool): All incoming connections are blocked.\"\"\"\n    stealth_mode_enabled = db.Column(db.Boolean)\n    \"\"\"stealth_mode_enabled (bool): Stealth mode is enabled.\"\"\"\n\n    # ActivationLockBypassCode\n    activation_lock_escrow_key = db.Column(db.String)\n    \"\"\"activation_lock_escrow_key (str): The activation lock bypass code generated by the device\"\"\"\n\n    # DEP Fetch/Sync Fields\n    is_dep = db.Column(db.Boolean)\n    \"\"\"is_dep (bool): This device has been synced from DEP. False indicates a manual or AC2 enrolment\"\"\"\n\n    description = db.Column(db.String)\n    \"\"\"description (str): The DEP description which is often identical to the SKU description on the invoice.\"\"\"\n    color = db.Column(db.String)\n    \"\"\"color: (str): The device color indicated by DEP\"\"\"\n    asset_tag = db.Column(db.String)\n    \"\"\"asset_tag (str): The device asset tag, if provided by Apple.\"\"\"\n    profile_status = db.Column(db.String)\n    \"\"\"profile_status (str): The status of profile installation: empty, assigned, pushed or removed.\"\"\"\n    profile_uuid = db.Column(db.String)\n    \"\"\"profile_uuid (str): The UUID of the assigned DEP profile\"\"\"\n    profile_assign_time = db.Column(db.DateTime)\n    \"\"\"profile_assign_time (datetime): The date and time indicating when the DEP profile was assigned\"\"\"\n    profile_push_time = db.Column(db.DateTime)\n    \"\"\"profile_push_time (datetime): The date and time indicating when the DEP profile was pushed.\"\"\"\n    device_assigned_date = db.Column(db.DateTime)\n    \"\"\"device_assigned_date (datetime): The date and time the device was recorded into DEP.\"\"\"\n    device_assigned_by = db.Column(db.String)\n    \"\"\"device_assigned_by (str): The email of the person who assigned the device.\"\"\"\n    os = db.Column(db.String)\n    \"\"\"os (str): The device operating system returned by DEP: iOS, OSX or tvOS\"\"\"\n    device_family = db.Column(db.String)\n    \"\"\"device_family (str): The device's Apple product family returned by DEP.\"\"\"\n\n    # TODO: Blocked Applications\n\n    @hybrid_property\n    def token(self):\n        return self._token if self._token is None else base64.b64decode(self._token)\n\n    @token.setter\n    def token(self, value):\n        self._token = base64.b64encode(value) if value is not None else None\n\n    @property\n    def hex_token(self):\n        \"\"\"Retrieve the device token in hex encoding, necessary for the APNS2 client.\"\"\"\n        if self._token is None:\n            return self._token\n        else:\n            return hexlify(self.token).decode('utf8')\n\n    certificate_id = db.Column(db.Integer, db.ForeignKey('certificates.id'))\n    certificate = db.relationship('Certificate', backref='devices')\n\n    dep_profile_id = db.Column(db.Integer, db.ForeignKey('dep_profiles.id'))\n    dep_profile = db.relationship('DEPProfile', backref='devices')\n\n    tags = db.relationship(\n        'Tag',\n        secondary=device_tags,\n        back_populates='devices'\n    )\n\n    _unlock_token = db.Column(db.String(), name='unlock_token', nullable=True)\n\n    @property\n    def unlock_token(self):\n        return self._unlock_token\n\n    @unlock_token.setter\n    def unlock_token(self, value):\n        if isinstance(value, NSData):\n            self._unlock_token = NSData.encode('base64')\n        else:\n            self._unlock_token = value\n\n    @property\n    def platform(self) -> Platform:\n        if self.model_name in ['iMac', 'MacBook Pro', 'MacBook Air', 'Mac Pro']:  # TODO: obviously not sufficient\n            return Platform.macOS\n        elif self.model_name in ['iPhone', 'iPad']:\n            return Platform.iOS\n        else:\n            return Platform.Unknown\n\n    def __repr__(self):\n        return '<Device ID=%r UDID=%r SerialNo=%r>' % (self.id, self.udid, self.serial_number)\n\n\nclass CommandSequence(db.Model):\n    \"\"\"A command sequence represents a series of commands where all members must succeed in order for the sequence to\n    succeed. I.E a single failure or timeout in the sequence stops the delivery of every other member.\n\n    :table: command_sequences\n    \"\"\"\n    __tablename__ = 'command_sequences'\n\n    id = db.Column(db.Integer, primary_key=True)\n\n\nclass Command(db.Model):\n    \"\"\"The command model represents a single MDM command that should be, has been, or has failed to be delivered to\n    a single enrolled device.\n    \n    :table: commands\n    \"\"\"\n    __tablename__ = 'commands'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (int): ID\"\"\"\n    request_type = db.Column(db.String, nullable=False)  # string representation of our local command handler\n    \"\"\"request_type (str): The command RequestType attribute\"\"\"\n    uuid = db.Column(GUID, index=True, unique=True, nullable=False)\n    \"\"\"uuid (GUID): Globally unique command UUID\"\"\"\n    parameters = db.Column(MutableDict.as_mutable(JSONEncodedDict),\n                           nullable=True)  # JSON add'l data as input to command builder\n    \"\"\"parameters (str): The parameters that were used when generating the command, serialized into JSON. Omitting the\n            RequestType and CommandUUID attributes.\"\"\"\n    status = db.Column(db.Enum(CommandStatus), index=True, nullable=False, default=CommandStatus.Queued)\n    \"\"\"status (CommandStatus): The status of the command.\"\"\"\n    queued_at = db.Column(db.DateTime, default=datetime.datetime.utcnow(), server_default=db.text('CURRENT_TIMESTAMP'))\n    \"\"\"queued_at (datetime.datetime): The datetime (utc) of when the command was created. Defaults to UTC now\"\"\"\n    sent_at = db.Column(db.DateTime, nullable=True)\n    \"\"\"sent_at (datetime.datetime): The datetime (utc) of when the command was delivered to the client.\"\"\"\n    acknowledged_at = db.Column(db.DateTime, nullable=True)\n    \"\"\"acknowledged_at (datetime.datetime): The datetime (utc) of when the Acknowledged, Error or NotNow response was\n        returned.\"\"\"\n    # command must only be sent after this date\n    after = db.Column(db.DateTime, nullable=True)\n    \"\"\"after (datetime.datetime): If not null, the command must not be sent until this datetime is in the past.\"\"\"\n\n    # number of retries remaining until dead\n    ttl = db.Column(db.Integer, nullable=False, default=5)\n    \"\"\"ttl (int): The number of retries remaining until the command will be dead/expired.\"\"\"\n\n    device_id = db.Column(db.ForeignKey('devices.id'), nullable=True)\n    \"\"\"device_id (int): The device ID on the devices table.\"\"\"\n    device = db.relationship('Device', backref='commands')\n    \"\"\"device (Device): The instance of the related device.\"\"\"\n\n    # device_user_id = db.Column(ForeignKey('device_users.id'), nullable=True)\n    # device_user = relationship('DeviceUser', backref='commands')\n\n    @classmethod\n    def from_model(cls, cmd: commands.Command):\n        \"\"\"This method turns a subclass of commands.Command into an SQLAlchemy model.\n        The parameters of the command are encoded as a JSON dictionary inside the parameters column.\n\n        Args:\n              cmd (commands.Command): The command to be turned into a database model.\n        Returns:\n              Command: The database model, ready to be committed.\n        \"\"\"\n        c = cls()\n        assert cmd.request_type is not None\n        c.request_type = cmd.request_type\n        c.uuid = cmd.uuid\n        c.parameters = cmd.parameters\n\n        return c\n\n    @classmethod\n    def find_by_uuid(cls, uuid: str):\n        \"\"\"Find and return an instance of the Command model matching the given UUID string.\n        \n        Args:\n              uuid (str): The command UUID\n              \n        Returns:\n              Command: Instance of the command, if any\n        \"\"\"\n        return cls.query.filter(cls.uuid == uuid).one()\n\n    @classmethod\n    def next_command(cls, device: Device):\n        \"\"\"Get the next available command in the queue for the specified device.\n\n        The next available command must match these predicates:\n\n        - Assigned to this device.\n        - The status is \"Queued\".\n        - The `after` field is in the past, or empty.\n\n        Args:\n            device (Device): The database model matching the device checking in.\n\n        Returns:\n            Command: The next command model to be processed.\n        \"\"\"\n        # d == d AND (q_status == Q OR (q_status == R AND result == 'NotNow'))\n        return cls.query.filter(db.and_(\n            cls.device == device,\n            cls.status == CommandStatus.Queued.value)).order_by(cls.id).first()\n\n    @classmethod\n    def next(cls, device: Device):  # type: (Type[Command], Device) -> Optional[Command]\n        model = cls.query.filter(db.and_(\n            cls.device == device,\n            cls.status == CommandStatus.Queued.value)).order_by(cls.id).first()\n\n\n    def __repr__(self):\n        return '<Command ID=%r UUID=%r qstatus=%r>' % (self.id, self.uuid, self.status)\n\n\nclass DeviceUser(db.Model):\n    \"\"\"\n    This model represents a managed user from the standpoint of the MDM.\n    It exists to support the macOS user channel extension.\n\n    :table: device_users\n    \"\"\"\n    __tablename__ = 'device_users'\n\n    id = db.Column(db.Integer, primary_key=True)\n\n    device_id = db.Column(db.ForeignKey('devices.id'), nullable=True)\n    \"\"\"(int): Device foreign key ID.\"\"\"\n    device = db.relationship('Device', backref='device_users')\n    \"\"\"(db.relationship): Device relationship\"\"\"\n    device_udid = db.Column(db.String(40), nullable=False)\n    \"\"\"(GUID): Device UDID\"\"\"\n    user_id = db.Column(GUID, nullable=False)\n    \"\"\"user_id (GUID): Local user's GUID, or network user's GUID from Directory Record.\"\"\"\n    long_name = db.Column(db.String)\n    \"\"\"long_name (str): The full name of the user\"\"\"\n    short_name = db.Column(db.String)\n    \"\"\"short_name (str): The short (username) of the user\"\"\"\n    need_sync_response = db.Column(db.Boolean)  # This is kind of transitive but added anyway.\n    user_configuration = db.Column(db.Boolean)\n    digest_challenge = db.Column(db.String)\n    auth_token = db.Column(db.String)\n\n\nclass Organization(db.Model):\n    \"\"\"The MDM home organization configuration.\n    \n    These attributes are used as the defaults for several other services where an org name is required.\n    Such as Certificate requests and Profile detail.\n    \n    :table: organizations\n    \"\"\"\n    __tablename__ = 'organizations'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (int): ID\"\"\"\n    name = db.Column(db.String)\n    \"\"\"name (string): Name\"\"\"\n    payload_prefix = db.Column(db.String)\n    \"\"\"payload_prefix (string): The reverse-dns style prefix to use for all generated profiles.\"\"\"\n\n    # http://www.ietf.org/rfc/rfc5280.txt\n    # maximum string lengths are well defined by this RFC and this schema follows those recommendations\n    # this x.509 name is used in the subject of the internal CA and issued certificates\n    x509_ou = db.Column(db.String(32))\n    \"\"\"x509_ou (string): The x.509 Organizational Unit for generating certificates.\"\"\"\n    x509_o = db.Column(db.String(64))\n    \"\"\"x509_o (string): The x.509 Organization for generating certificates.\"\"\"\n    x509_st = db.Column(db.String(128))\n    \"\"\"x509_st (string): The x.509 State for generating certificates.\"\"\"\n    x509_c = db.Column(db.String(2))\n    \"\"\"x509_c (string): The 2 letter x.509 country code for generating certificates. \"\"\"\n\n\nclass DeviceIdentitySources(Enum):\n    \"\"\"A list of sources for Device Identity.\"\"\"\n    InternalPKCS12 = 'internal_pkcs12'\n    InternalSCEP = 'internal_scep'\n    ExternalSCEP = 'external_scep'\n\n\nclass SCEPConfig(db.Model):\n    \"\"\"This table holds a single row containing information used to generate the SCEP enrollment profile.\n    \n    :table: scep_config\n    \n    See Also:\n          - `https://tools.ietf.org/html/rfc3280.html`_.\n    \"\"\"\n    __tablename__ = 'scep_config'\n\n    id = db.Column(db.Integer, primary_key=True)\n    source_type = db.Column(db.Enum(DeviceIdentitySources), default=DeviceIdentitySources.InternalSCEP)\n    \"\"\"source_type (DeviceIdentitySources): Specify the source used for device certificates.\"\"\"\n    url = db.Column(db.String, nullable=False)\n\n    challenge_enabled = db.Column(db.Boolean, default=False)\n    challenge = db.Column(db.String)\n    ca_fingerprint = db.Column(db.String)\n    subject = db.Column(db.String, nullable=False)  # eg. O=x/OU=y/CN=z\n    key_size = db.Column(db.Integer, default=2048, nullable=False)\n    key_type = db.Column(db.String, default='RSA', nullable=False)\n    key_usage = db.Column(db.Enum(KeyUsage), default=KeyUsage.All)\n\n    retries = db.Column(db.Integer, default=3, nullable=False)\n    retry_delay = db.Column(db.Integer, default=10, nullable=False)\n    certificate_renewal_time_interval = db.Column(db.Integer, default=14, nullable=False)\n\n\nclass SubjectAlternativeNameType(Enum):\n    \"\"\"Types of SubjectAlternativeNames that can be added using cryptography SAN extension.\n    \n    See Also:\n          - `https://tools.ietf.org/html/rfc3280.html`_.\n    \"\"\"\n\n    RFC822Name = 'RFC822Name'\n    \"\"\"E-mail address, see: https://tools.ietf.org/html/rfc822\"\"\"\n\n    DNSName = 'DNSName'\n    UniformResourceIdentifier = 'UniformResourceIdentifier'\n    DirectoryName = 'DirectoryName'\n    RegisteredID = 'RegisteredID'\n    IPAddress = 'IPAddress'\n    OtherName = 'OtherName'\n    # TODO: ntPrincipal\n\n\nclass SubjectAlternativeName(db.Model):\n    \"\"\"This table holds SANs included in the SCEP enrollment request.\n    \n    :table: subject_alternative_names\n    \"\"\"\n    __tablename__ = 'subject_alternative_names'\n\n    id = db.Column(db.Integer, primary_key=True)\n    discriminator = db.Column(db.Enum(SubjectAlternativeNameType), nullable=False)\n\n    str_value = db.Column(db.String)\n    octet_value = db.Column(db.LargeBinary)  # For IPAddress\n\n\nclass Tag(db.Model):\n    \"\"\"This table holds tags, which are categories that are many-to-many and polymorphic to different types of\n    objects.\"\"\"\n    __tablename__ = 'tags'\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String, nullable=False)\n    color = db.Column(db.String(6), default='888888')\n\n    # applications = db.relationship(\n    #     \"Application\",\n    #     secondary=application_tags,\n    #     back_populates=\"tags\",\n    # )\n\n    devices = db.relationship(\n        \"Device\",\n        secondary=device_tags,\n        back_populates=\"tags\",\n    )\n\n    # profiles = db.relationship(\n    #     \"Profiles\",\n    #     secondary=profile_tags,\n    #     back_populates=\"tags\",\n    # )\n\n\n"
  },
  {
    "path": "commandment/mutablelist.py",
    "content": "\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2017 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\"\"\"\ntry:\n    from sqlalchemy.ext.mutable import MutableList\nexcept ImportError:\n    # MutableList didn't make it into SQLAlchemy 1.0.12\n    # This function copied directly from SQLAlchemy source\n    from sqlalchemy.ext.mutable import Mutable\n\n    class MutableList(Mutable, list):\n        \"\"\"A list type that implements :class:`.Mutable`.\n\n        The :class:`.MutableList` object implements a list that will\n        emit change events to the underlying mapping when the contents of\n        the list are altered, including when values are added or removed.\n\n        Note that :class:`.MutableList` does **not** apply mutable tracking to  the\n        *values themselves* inside the list. Therefore it is not a sufficient\n        solution for the use case of tracking deep changes to a *recursive*\n        mutable structure, such as a JSON structure.  To support this use case,\n        build a subclass of  :class:`.MutableList` that provides appropriate\n        coersion to the values placed in the dictionary so that they too are\n        \"mutable\", and emit events up to their parent structure.\n\n        .. versionadded:: 1.1\n\n        .. seealso::\n\n            :class:`.MutableDict`\n\n            :class:`.MutableSet`\n\n        \"\"\"\n\n        def __setitem__(self, index, value):\n            \"\"\"Detect list set events and emit change events.\"\"\"\n            list.__setitem__(self, index, value)\n            self.changed()\n\n        def __setslice__(self, start, end, value):\n            \"\"\"Detect list set events and emit change events.\"\"\"\n            list.__setslice__(self, start, end, value)\n            self.changed()\n\n        def __delitem__(self, index):\n            \"\"\"Detect list del events and emit change events.\"\"\"\n            list.__delitem__(self, index)\n            self.changed()\n\n        def __delslice__(self, start, end):\n            \"\"\"Detect list del events and emit change events.\"\"\"\n            list.__delslice__(self, start, end)\n            self.changed()\n\n        def pop(self, *arg):\n            result = list.pop(self, *arg)\n            self.changed()\n            return result\n\n        def append(self, x):\n            list.append(self, x)\n            self.changed()\n\n        def extend(self, x):\n            list.extend(self, x)\n            self.changed()\n\n        def insert(self, i, x):\n            list.insert(self, i, x)\n            self.changed()\n\n        def remove(self, i):\n            list.remove(self, i)\n            self.changed()\n\n        def clear(self):\n            list.clear(self)\n            self.changed()\n\n        def sort(self):\n            list.sort(self)\n            self.changed()\n\n        def reverse(self):\n            list.reverse(self)\n            self.changed()\n\n        @classmethod\n        def coerce(cls, index, value):\n            \"\"\"Convert plain list to instance of this class.\"\"\"\n            if not isinstance(value, cls):\n                if isinstance(value, list):\n                    return cls(value)\n                return Mutable.coerce(index, value)\n            else:\n                return value\n\n        def __getstate__(self):\n            return list(self)\n\n        def __setstate__(self, state):\n            self[:] = state\n"
  },
  {
    "path": "commandment/omdm/__init__.py",
    "content": "from flask import Blueprint, current_app\nfrom uuid import uuid4\nimport plistlib\n\nomdm_app = Blueprint('omdm_app', __name__)\n\n\n@omdm_app.route('/')\ndef omdm():\n    faux_command = {\n        'CommandUUID': str(uuid4()),\n        'RequestType': 'OMAlert',\n        'Message': 'Hello World!'\n    }\n\n    return plistlib.dumps(faux_command), {'Content-Type': 'text/xml'}"
  },
  {
    "path": "commandment/omdm/models.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n\nfrom sqlalchemy import Integer, String, ForeignKey, Table, Text, Boolean, DateTime, Enum as DBEnum, text, \\\n    BigInteger, and_, or_, LargeBinary, Float\n\n"
  },
  {
    "path": "commandment/pkg/__init__.py",
    "content": "from enum import Enum\n\n\nclass ManifestAssetKind(Enum):\n    SoftwarePackage = 'software-package'\n    FullSizeImage = 'full-size-image'\n    DisplayImage = 'display-image'\n\n"
  },
  {
    "path": "commandment/pkg/appmanifest.py",
    "content": "import argparse\nfrom typing import List, Tuple, Optional\nfrom bixar.archive import XarFile\nfrom xml.etree import ElementTree\nimport plistlib\nimport hashlib\nimport os.path\n\nPackages = List[Tuple[str, str]]\nBundles = List[Tuple[str, str]]\nMD5_CHUNK_SIZE = 10 << 20\n\n\ndef blow_chunks(fileobj) -> Tuple[str, List[str]]:\n    fileobj.seek(0)\n    chunks = []\n    total_hash = hashlib.md5()\n    \n    for chunk in iter(lambda: fileobj.read(MD5_CHUNK_SIZE), b''):\n        new_hash = hashlib.md5()\n        new_hash.update(chunk)\n        total_hash.update(chunk)\n        chunks.append(new_hash.hexdigest())\n\n    return total_hash.hexdigest(), chunks\n\n\ndef url_from_metadata(path: str) -> Optional[str]:\n    \"\"\"Try to determine the download URL from the spotlight attributes if the local machine is a mac.\"\"\"\n    try:\n        from Foundation import NSFileManager, NSPropertyListSerialization\n    except:\n        return None\n\n    fm = NSFileManager.defaultManager()\n    attrs, err = fm.attributesOfItemAtPath_error_(path, None)\n    if err:\n        return None\n\n    if 'NSFileExtendedAttributes' not in attrs:\n        return None\n\n    extd_attrs = attrs['NSFileExtendedAttributes']\n\n    if 'com.apple.metadata:kMDItemWhereFroms' not in extd_attrs:\n        return None\n    else:\n        plist_data: bytes = extd_attrs['com.apple.metadata:kMDItemWhereFroms']\n        value: List[str] = plistlib.loads(plist_data)\n        if len(value) > 0:\n            return value.pop(0)\n        else:\n            return None\n\n\ndef main():\n    parser = argparse.ArgumentParser(description='Create an application manifest')\n    parser.add_argument('source',\n                        help='Source pkg [REQUIRED!]',\n                        metavar='filename')\n\n    args = parser.parse_args()\n\n    archive = XarFile(path=args.source)\n    distribution = archive.extract_bytes('Distribution')\n    package_info = archive.extract_bytes('PackageInfo')\n    packages: Packages = []\n    bundles: Bundles = []\n    file_size = os.path.getsize(args.source)\n    title = os.path.basename(args.source)\n\n    if distribution:\n        el = ElementTree.fromstring(distribution)\n        title = el.findtext('.//title')\n        for pkgRef in el.iter('pkg-ref'):\n            if 'version' in pkgRef.attrib:\n                packages.append((pkgRef.attrib['id'], pkgRef.attrib['version']))\n\n        bundles = [(b.attrib['id'], b.attrib['CFBundleVersion']) for b in el.iter('bundle')]\n\n    if package_info:\n        el = ElementTree.fromstring(package_info)\n        for pkgInfo in el.iter('pkg-info'):\n            packages.append((pkgInfo.attrib['identifier'], pkgInfo.attrib['version']))\n\n    with open(args.source, 'rb') as fd:\n        total_hash, chunks = blow_chunks(fd)\n\n    url = url_from_metadata(args.source)\n\n    manifest = {\n        'items': [{\n            'assets': [{\n                'kind': 'software-package',\n                'md5-size': MD5_CHUNK_SIZE,\n                'md5s': chunks,\n                'url': '{}'.format(url) if url else 'https://package/url/here.pkg'\n            }],\n            'metadata': {\n                'kind': 'software',\n                'title': title,\n                'sizeInBytes': file_size,\n                'bundle-identifier': '',\n                'bundle-version': ''\n            }\n        }]\n    }\n\n    pkgs_bundles = [{'bundle-identifier': i[0], 'bundle-version': i[1]} for i in packages]\n    manifest['items'][0]['metadata'].update(pkgs_bundles[0])\n\n    if len(bundles) > 1:\n        manifest['items'][0]['metadata']['items'] = [{'bundle-identifier': i[0], 'bundle-version': i[1]} for i in bundles]\n\n    print(plistlib.dumps(manifest).decode('utf8'))\n\n"
  },
  {
    "path": "commandment/pkg/manifest.py",
    "content": "from typing import List, Union\nimport hashlib\nimport io\n\n# Required for InstallApplication to work.\nDEFAULT_MD5_CHUNK_SIZE = 10485760\n\n\ndef chunked_hash(stream: Union[io.RawIOBase, io.BufferedIOBase], chunk_size: int = DEFAULT_MD5_CHUNK_SIZE) -> List[bytes]:\n    \"\"\"Create a list of hashes of chunk_size size in bytes.\n\n    Args:\n          stream (Union[io.RawIOBase, io.BufferedIOBase]): The steam containing the bytes to be hashed.\n          chunk_size (int): The md5 chunk size. Default is 10485760 (which is required for InstallApplication).\n\n    Returns:\n          List[str]: A list of md5 hashes calculated for each chunk\n    \"\"\"\n    chunk = stream.read(chunk_size)\n    hashes = []\n\n    while chunk is not None:\n        h = hashlib.md5()\n        h.update(chunk)\n        md5 = h.digest()\n        hashes.append(md5)\n        chunk = stream.read(chunk_size)\n\n    return hashes\n"
  },
  {
    "path": "commandment/pkg/old_app_manifest.py",
    "content": "\"\"\"\nCopyright (c) 2015 Jesse Peterson\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\"\"\"\n\nimport subprocess\nfrom tempfile import mkdtemp\nimport os\nfrom xml.dom.minidom import parse, parseString\nfrom hashlib import md5\nimport plistlib\n\n# use system PATH\nXAR_PATH = 'xar'\n\nMD5_CHUNK_SIZE = 1024 * 1024 * 10  # 10 MiB\n\n\ndef pkg_signed(filename):\n    xar_args = [XAR_PATH,\n                '-t',  # only test archive\n                '--dump-toc=-',\n                '-f',\n                filename]\n\n    p = subprocess.Popen(xar_args, stdout=subprocess.PIPE)\n    toc, _ = p.communicate()\n\n    if p.returncode != 0:\n        return False\n\n    toc_md = parseString(toc)\n\n    # for purposes of checking just see if the xar TOC has an X509Certificate element\n    return len(toc_md.getElementsByTagName('X509Certificate')) > 0\n\n\ndef get_pkg_bundle_ids(filename):\n    '''Get metadata from Distribution or PackageInfo inside of pkg'''\n\n    tmp_dir = mkdtemp()\n\n    print('Extracting Distribution/PackageInfo file to', tmp_dir)\n\n    xar_args = [XAR_PATH,\n                '-x',  # extract switch\n                '--exclude', '/',  # exclude any files in subdirectories\n                '-C', tmp_dir,  # extract to our temporary directory\n                '-f', filename,  # extract this specific file\n                'Distribution', 'PackageInfo']  # files to extract\n\n    rtn = subprocess.call(xar_args)\n\n    tmp_dist_file = os.path.join(tmp_dir, 'Distribution')\n    tmp_pinf_file = os.path.join(tmp_dir, 'PackageInfo')\n\n    pkgs = []\n    bundles = []\n\n    # for non-PackageInfo packages (use PackageInfo)\n    if os.path.exists(tmp_dist_file):\n        # use XML minidom to parse a Distribution file\n        dist_md = parse(tmp_dist_file)\n\n        # capture the pkg IDs and versions by searching for 'pkg-ref' elements\n        # which include a 'version' attribute on them. append them to our list\n        for i in dist_md.getElementsByTagName('pkg-ref'):\n            if i.hasAttribute('version'):\n                pkgs.append((i.getAttribute('id'), i.getAttribute('version')))\n\n        # capture the bundle IDs and versions by searching for 'bundle'\n        # elements which we're searching for a 'CFBundleVersion' attribute on\n        # them. append them to our list\n        for i in dist_md.getElementsByTagName('bundle'):\n            bundles.append((i.getAttribute('id'), i.getAttribute('CFBundleVersion')))\n\n        print('Removing Distribution file')\n        os.unlink(tmp_dist_file)\n\n    # for non-Distribution packages (use the PackageInfo)\n    if os.path.exists(tmp_pinf_file):\n        pinf_md = parse(tmp_pinf_file)\n\n        # capture the pkg ID and version by searching for a pkg-info element\n        # and using the identifier and version attributes\n        for i in pinf_md.getElementsByTagName('pkg-info'):\n            pkgs.append((i.getAttribute('identifier'), i.getAttribute('version')))\n\n        print('Removing PackageInfo file')\n        os.unlink(tmp_pinf_file)\n\n    print('Removing temp directory')\n    os.rmdir(tmp_dir)\n\n    return (pkgs, bundles)\n\n\ndef get_chunked_md5(filename, chunksize=MD5_CHUNK_SIZE):\n    h = md5()\n    md5s = []\n    total_hash = md5()\n    with open(filename, 'rb') as f:\n        for chunk in iter(lambda: f.read(chunksize), b''):\n            new_hash = md5()\n            new_hash.update(chunk)\n            total_hash.update(chunk)\n            md5s.append(new_hash.hexdigest())\n\n    return (total_hash.hexdigest(), md5s)\n"
  },
  {
    "path": "commandment/pkg/schema.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass Asset(Schema):\n    kind = fields.String(default='software-package')\n    md5_size = fields.Integer(default=10485760)\n    md5s = fields.List(fields.String())\n    url = fields.URL()\n    needs_shine = fields.Boolean()\n    \n\nclass BundleItem(Schema):\n    bundle_identifier = fields.String()\n    bundle_version = fields.String()\n\n\nclass Metadata(Schema):\n    bundle_identifier = fields.String()\n    bundle_version = fields.String()\n    items = fields.Nested(BundleItem, many=True)\n    kind = fields.String()\n    sizeInBytes = fields.String()\n    subtitle = fields.String()\n    title = fields.String()\n\n\nclass ManifestItem(Schema):\n    assets = fields.Nested(Asset, many=True)\n    metadata = fields.Nested(Metadata)\n\n\nclass Manifest(Schema):\n    items = fields.Nested(ManifestItem, many=True)\n\n"
  },
  {
    "path": "commandment/pki/ca.py",
    "content": "from flask import g, current_app\nimport sqlalchemy.orm.exc\n\nfrom .models import CertificateAuthority\nfrom commandment.models import db, Device\nfrom commandment.pki.models import CertificateType, Certificate\n\n\ndef get_ca() -> CertificateAuthority:\n    if 'ca' not in g:\n        try:\n            ca = db.session.query(CertificateAuthority).filter_by(common_name='COMMANDMENT-CA').one()\n        except sqlalchemy.orm.exc.NoResultFound:\n            ca = CertificateAuthority.create()\n\n        g.ca = ca\n\n    return g.ca\n\n#\n# @current_app.teardown_appcontext\n# def teardown_ca():\n#     ca = g.pop('ca', None)\n\n"
  },
  {
    "path": "commandment/pki/models.py",
    "content": "\"\"\"\nThis module contains the SQLAlchemy models for PKI related functionality.\n\"\"\"\nfrom enum import Enum\n\nfrom commandment.models import db\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes, serialization\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography import x509\nfrom cryptography.x509 import NameOID, DNSName\nimport datetime\n\n\nclass CertificateAuthority(db.Model):\n    \"\"\"Certificate authority storage: database implementation.\n\n    I'm loathe to create a model tied to the storage implementation but this was the easiest option at the time.\n    \"\"\"\n    __tablename__ = 'certificate_authority'\n\n    id = db.Column(db.Integer, primary_key=True)\n    common_name = db.Column(db.String, unique=True)\n    serial = db.Column(db.Integer, default=0)\n    validity_period = db.Column(db.Integer, default=365)\n\n    certificate_id = db.Column(db.Integer, db.ForeignKey('certificates.id'))\n    certificate = db.relationship('CACertificate', backref='certificate_authority')\n\n    rsa_private_key_id = db.Column(db.Integer, db.ForeignKey('rsa_private_keys.id'))\n    rsa_private_key = db.relationship('RSAPrivateKey', backref='certificate_authority')\n\n    @classmethod\n    def create(cls, common_name: str = 'COMMANDMENT-CA', key_size=2048):\n        ca = cls()\n        ca.common_name = common_name\n        name = x509.Name([\n            x509.NameAttribute(NameOID.COMMON_NAME, common_name),\n            x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'commandment')\n        ])\n\n        private_key = rsa.generate_private_key(\n            public_exponent=65537,\n            key_size=key_size,\n            backend=default_backend(),\n        )\n        ca.rsa_private_key = RSAPrivateKey.from_crypto(private_key)\n        db.session.add(ca.rsa_private_key)\n\n        certificate = x509.CertificateBuilder().subject_name(\n            name\n        ).issuer_name(\n            name\n        ).public_key(\n            private_key.public_key()\n        ).serial_number(\n            x509.random_serial_number()\n        ).not_valid_before(\n            datetime.datetime.utcnow()\n        ).not_valid_after(\n            datetime.datetime.utcnow() + datetime.timedelta(days=365)\n        ).add_extension(\n            x509.BasicConstraints(ca=True, path_length=None), True\n        ).sign(private_key, hashes.SHA256(), default_backend())\n\n        ca_certificate_model = CACertificate.from_crypto(certificate)\n        ca_certificate_model.rsa_private_key = ca.rsa_private_key\n        ca.certificate = ca_certificate_model\n\n        db.session.add(ca)\n        db.session.commit()\n\n        return ca\n\n    def create_device_csr(self, common_name: str) -> (rsa.RSAPrivateKeyWithSerialization, x509.CertificateSigningRequest):\n        \"\"\"\n        Create a Certificate Signing Request with the specified Common Name.\n\n        The private key model is automatically committed to the database.\n        This is also true for the certificate signing request.\n\n        Args:\n            common_name (str): The certificate Common Name attribute\n\n        Returns:\n            Tuple[rsa.RSAPrivateKeyWithSerialization, x509.CertificateSigningRequest] - A tuple containing the RSA\n            Private key that was generated, along with the CSR.\n        \"\"\"\n        private_key = rsa.generate_private_key(\n            public_exponent=65537,\n            key_size=2048,\n            backend=default_backend(),\n        )\n\n        private_key_model = RSAPrivateKey.from_crypto(private_key)\n        db.session.add(private_key_model)\n\n        name = x509.Name([\n            x509.NameAttribute(NameOID.COMMON_NAME, common_name),\n            x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'commandment')\n        ])\n\n        builder = x509.CertificateSigningRequestBuilder()\n        builder = builder.subject_name(name)\n        builder = builder.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)\n\n        request = builder.sign(\n            private_key,\n            hashes.SHA256(),\n            default_backend()\n        )\n\n        csr_model = CertificateSigningRequest().from_crypto(request)\n        csr_model.rsa_private_key = private_key_model\n        db.session.add(csr_model)\n        db.session.commit()\n\n        return private_key, request\n\n    def sign(self, request: x509.CertificateSigningRequest) -> x509.Certificate:\n        \"\"\"\n        Sign a Certificate Signing Request.\n\n        The issued certificate is automatically persisted to the database.\n\n        Args:\n            request (x509.CertificateSigningRequest): The CSR object (cryptography) not the SQLAlchemy model.\n\n        Returns:\n            x509.Certificate: A signed certificate\n        \"\"\"\n        b = x509.CertificateBuilder()\n        self.serial += 1\n\n        private_key_model = self.rsa_private_key\n        private_key = private_key_model.to_crypto()\n        # ca_certificate_model = self.certificate\n        # ca_certificate = ca_certificate_model.to_crypto()\n\n        name = x509.Name([\n            x509.NameAttribute(NameOID.COMMON_NAME, self.common_name),\n            x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'commandment')\n        ])\n\n        cert = b.not_valid_before(\n            datetime.datetime.utcnow()\n        ).not_valid_after(\n            datetime.datetime.utcnow() + datetime.timedelta(days=self.validity_period)\n        ).serial_number(\n            self.serial\n        ).issuer_name(\n            name\n        ).subject_name(\n            request.subject\n        ).public_key(\n            request.public_key()\n        ).sign(private_key, hashes.SHA256(), default_backend())\n\n        # cert_model = DeviceIdentityCertificate().from_crypto(cert)\n        # db.session.add(cert_model)\n        # db.session.commit()\n\n        return cert\n\n\nclass CertificateType(Enum):\n    \"\"\"A list of the polymorphic identities available for subclasses of Certificate.\n\n    The enumerated type hints what the certificate is intended to be used for.\n    \"\"\"\n    CSR = 'csr'\n    PUSH = 'mdm.pushcert'\n    ENCRYPT = 'mdm.encrypt'\n    WEB = 'mdm.webcrt'\n    CA = 'mdm.cacert'\n    DEVICE = 'mdm.device'\n    STOKEN = 'dep.stoken'\n    ANCHOR = 'dep.anchor'\n    SUPERVISION = 'dep.supervision'\n\nclass Certificate(db.Model):\n    \"\"\"Polymorphic base for certificate types.\n\n    These certificate classes are only intended to be used for storing certificates related to running the MDM or\n    certificates issued by the MDM internal CA or SCEP service.\n\n    Note that X.509 name attributes have fixed lengths as defined in `RFC5280`_.\n\n    :table: certificates\n\n    .. _RFC5280:\n       http://www.ietf.org/rfc/rfc5280.txt\n    \"\"\"\n    __tablename__ = 'certificates'\n\n    id = db.Column(db.Integer, primary_key=True)\n    \"\"\"id (int): Primary Key\"\"\"\n    pem_data = db.Column(db.Text, nullable=False)\n    \"\"\"pem_data (str): PEM Encoded Certificate Data\"\"\"\n\n    rsa_private_key_id = db.Column(db.Integer, db.ForeignKey('rsa_private_keys.id'))\n    \"\"\"rsa_private_key_id (int): Foreign key reference to an RSAPrivateKey IF the private key was generated by us.\"\"\"\n    rsa_private_key = db.relationship(\n        'RSAPrivateKey',\n        backref='certificates',\n    )\n\n    x509_cn = db.Column(db.String(64), nullable=True)\n    \"\"\"x509_cn (str): X.509 Common Name\"\"\"\n    x509_ou = db.Column(db.String(32))\n    \"\"\"x509_ou (str): X.509 Organizational Unit\"\"\"\n    x509_o = db.Column(db.String(64))\n    \"\"\"x509_o (str): X.509 Organization\"\"\"\n    x509_c = db.Column(db.String(2))\n    \"\"\"x509_c (str): X.509 2 letter Country Code\"\"\"\n    x509_st = db.Column(db.String(128))\n    \"\"\"x509_st (str): X.509 State or Location\"\"\"\n    not_before = db.Column(db.DateTime(timezone=False), nullable=False)\n    \"\"\"not_before (datetime): Certificate validity - not before\"\"\"\n    not_after = db.Column(db.DateTime(timezone=False), nullable=False)\n    \"\"\"not_after (datetime): Certificate validity - not after\"\"\"\n    serial = db.Column(db.BigInteger)\n    \"\"\"serial (int): Serial Number\"\"\"\n    # SHA-256 hash of DER-encoded certificate\n    fingerprint = db.Column(db.String(64), nullable=False, index=True, unique=True)  # Unique\n    \"\"\"fingerprint (str): SHA-256 hash of certificate\"\"\"\n    push_topic = db.Column(db.String, nullable=True)  # Only required for push certificate\n    \"\"\"push_topic (str): Only present for Push Certificates, the x.509 User ID field value\"\"\"\n    discriminator = db.Column(db.String(20))\n    \"\"\"discriminator (str): The type of certificate\"\"\"\n\n    __mapper_args__ = {\n        'polymorphic_on': discriminator,\n        'polymorphic_identity': 'certificates',\n    }\n\n    @classmethod\n    def from_crypto_type(cls, certificate: x509.Certificate, certtype: CertificateType):\n        # type: (certtype, x509.Certificate, CertificateType) -> Certificate\n        m = cls()\n        m.serial = certificate.serial_number\n        m.pem_data = certificate.public_bytes(serialization.Encoding.PEM)\n        m.not_after = certificate.not_valid_after\n        m.not_before = certificate.not_valid_before\n        m.fingerprint = certificate.fingerprint(hashes.SHA256())\n        m.discriminator = certtype.value\n        m.serial = str(certificate.serial_number)\n\n        subject: x509.Name = certificate.subject\n        cns = subject.get_attributes_for_oid(NameOID.COMMON_NAME)\n        if cns is not None:\n            m.x509_cn = cns[0].value\n\n        return m\n\n\nclass RSAPrivateKey(db.Model):\n    \"\"\"RSA Private Key Model\"\"\"\n    __tablename__ = 'rsa_private_keys'\n\n    #: id db.Column\n    id = db.Column(db.Integer, primary_key=True)\n    pem_data = db.Column(db.Text, nullable=False)\n\n    @classmethod\n    def from_crypto(cls, private_key: rsa.RSAPrivateKeyWithSerialization):\n        \"\"\"Convert a cryptography RSAPrivateKey object to an SQLAlchemy model.\"\"\"\n        # type: (type, rsa.RSAPrivateKeyWithSerialization) -> RSAPrivateKey\n        m = cls()\n        m.pem_data = private_key.private_bytes(\n            encoding=serialization.Encoding.PEM,\n            format=serialization.PrivateFormat.PKCS8,\n            encryption_algorithm=serialization.NoEncryption(),\n        )\n\n        return m\n\n    def to_crypto(self) -> rsa.RSAPrivateKey:\n        \"\"\"Convert an SQLAlchemy RSAPrivateKey model to a cryptography RSA Private Key.\"\"\"\n        pk = serialization.load_pem_private_key(\n            self.pem_data,\n            backend=default_backend(),\n            password=None,\n        )\n        return pk\n\n\nclass CertificateSigningRequest(Certificate):\n    \"\"\"Polymorphic single table inheritance specifically for Certificate Signing Requests.\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.CSR.value\n    }\n\n    @classmethod\n    def from_crypto(cls, csr: x509.CertificateSigningRequest):\n        # type: (type, x509.CertificateSigningRequest, CertificateType) -> Certificate\n        m = cls()\n        m.pem_data = csr.public_bytes(serialization.Encoding.PEM)\n        m.not_before = datetime.datetime.utcnow()\n        m.not_after = datetime.datetime.utcnow() + datetime.timedelta(days=700)\n        h = hashes.Hash(hashes.SHA256(), default_backend())\n        h.update(m.pem_data)\n        m.fingerprint = h.finalize()\n\n        m.discriminator = CertificateType.CSR.value\n\n        subject: x509.Name = csr.subject\n        cns = subject.get_attributes_for_oid(NameOID.COMMON_NAME)\n        if cns is not None:\n            m.x509_cn = cns[0].value\n\n        return m\n\n\nclass SSLCertificate(Certificate):\n    \"\"\"Polymorphic single table inheritance specifically for SSL certificates assigned to the MDM for HTTPS traffic.\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.WEB.value\n    }\n\n\nclass PushCertificate(Certificate):\n    \"\"\"Polymorphic single table inheritance specifically for APNS MDM Push Certificates assigned to the MDM.\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.PUSH.value\n    }\n\n    @classmethod\n    def from_crypto(cls, certificate: x509.Certificate):\n        m = Certificate.from_crypto_type(certificate, CertificateType.PUSH)\n        return m\n\n\nclass CACertificate(Certificate):\n    \"\"\"Polymorphic single table inheritance specifically for Certificate Authorities generated by this MDM.\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.CA.value\n    }\n\n    @classmethod\n    def from_crypto(cls, certificate: x509.Certificate):  # type: () -> CACertificate\n        m = cls.from_crypto_type(certificate, CertificateType.CA)\n        return m\n\n\nclass DeviceIdentityCertificate(Certificate):\n    \"\"\"Polymorphic single table inheritance specifically for device identity certificates.\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.DEVICE.value\n    }\n\n    @classmethod\n    def from_crypto(cls, certificate: x509.Certificate):\n        m = cls()\n        m.serial = certificate.serial_number\n        m.pem_data = certificate.public_bytes(encoding=serialization.Encoding.PEM)\n        m.not_after = certificate.not_valid_after\n        m.not_before = certificate.not_valid_before\n        m.fingerprint = certificate.fingerprint(hashes.SHA256())\n\n        subject: x509.Name = certificate.subject\n\n        cns = subject.get_attributes_for_oid(NameOID.COMMON_NAME)\n        if cns is not None:\n            m.x509_cn = cns[0].value\n\n        # m.x509_c = subject.get_attributes_for_oid(NameOID.COUNTRY_NAME)\n        # m.x509_o = subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)\n        # m.x509_ou = subject.get_attributes_for_oid(NameOID.ORGANIZATIONAL_UNIT_NAME)\n        # m.x509_st = subject.get_attributes_for_oid(NameOID.STATE_OR_PROVINCE_NAME)\n\n        return m\n\n\nclass EncryptionCertificate(Certificate):\n    \"\"\"Polymorphic single table inheritance specifically for Encryption Certificates\"\"\"\n    __mapper_args__ = {\n        'polymorphic_identity': CertificateType.ENCRYPT.value\n    }\n\n    @classmethod\n    def from_crypto(cls, certificate: x509.Certificate):\n        # TODO: sometimes serial numbers are too large even for SQLite BIGINT\n        m = cls()\n        m.serial = certificate.serial_number\n        m.pem_data = certificate.public_bytes(encoding=serialization.Encoding.PEM)\n        m.not_after = certificate.not_valid_after\n        m.not_before = certificate.not_valid_before\n        m.fingerprint = certificate.fingerprint(hashes.SHA256())\n\n        subject: x509.Name = certificate.subject\n\n        cns = subject.get_attributes_for_oid(NameOID.COMMON_NAME)\n        if cns is not None:\n            m.x509_cn = cns[0].value\n        return m"
  },
  {
    "path": "commandment/pki/openssl.py",
    "content": "# Regrettably, some functionality must come from PyOpenSSL\nfrom typing import Optional\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import serialization\nimport OpenSSL\n\n\ndef create_pkcs12(\n        private_key: rsa.RSAPrivateKeyWithSerialization,\n        certificate: x509.Certificate,\n        passphrase: Optional[str] = None) -> Optional[bytes]:\n    \"\"\"Create a PKCS#12 container from the given RSA key and Certificate.\"\"\"\n\n    p12 = OpenSSL.crypto.PKCS12()\n    pkey = OpenSSL.crypto.PKey.from_cryptography_key(private_key)\n    p12.set_privatekey(pkey)\n    cert = OpenSSL.crypto.X509.from_cryptography(certificate)\n    p12.set_certificate(cert)\n\n    return p12.export(passphrase)\n\n"
  },
  {
    "path": "commandment/pki/ormutils.py",
    "content": "from typing import Optional\n\nfrom asn1crypto.cms import ContentInfo, EnvelopedData, KeyTransRecipientInfo, RecipientIdentifier\n\nfrom commandment.pki.models import Certificate\n\n\ndef find_recipient(cms_data: bytes) -> Optional[Certificate]:\n    \"\"\"Find the Certificate + Private Key of a recipient indicated by encoded CMS/PKCS#7 data from the database and\n    return the database model that matches (if any).\n\n    Requires that the indicated recipient is present in the `certificates` table, and has a matching private key in the\n    `rsa_private_keys` table.\n    \"\"\"\n    content_info = ContentInfo.load(cms_data)\n\n    assert content_info['content_type'].native == 'enveloped_data'\n    content: EnvelopedData = content_info['content']\n\n    for recipient_info in content['recipient_infos']:\n        if recipient_info.name == 'ktri':  # KeyTransRecipientInfo\n            recipient: KeyTransRecipientInfo = recipient_info.chosen\n            recipient_id: RecipientIdentifier = recipient['rid']\n            assert recipient_id.name == 'issuer_and_serial_number'\n\n        else:\n            pass  # Unsupported recipient type\n\n    return None\n\n"
  },
  {
    "path": "commandment/pki/serialization.py",
    "content": "from cryptography.hazmat.backends import default_backend\nfrom cryptography import x509\nfrom cryptography.x509.oid import NameOID\nfrom cryptography.hazmat.primitives import serialization, hashes\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom asn1crypto import pkcs12, pem, x509 as asn1x509\n\n\n# cryptography helper functions\n\ndef from_pem(pem_data: str) -> x509.Certificate:\n    return x509.load_pem_x509_certificate(pem_data, default_backend())\n\n\ndef from_der(der_data: bytes) -> x509.Certificate:\n    return x509.load_der_x509_certificate(der_data, default_backend())\n\n\ndef rsa_from_der(rsa_der_data: bytes, password: str = None) -> rsa.RSAPrivateKeyWithSerialization:\n    return serialization.load_der_private_key(\n        rsa_der_data,\n        password,\n        default_backend()\n    )\n\n\ndef rsa_from_pem(rsa_pem_data: bytes, password: str = None) -> rsa.RSAPrivateKeyWithSerialization:\n    return serialization.load_pem_private_key(\n        rsa_pem_data,\n        password,\n        default_backend()\n    )\n\n\ndef rsa_to_pem(key: rsa.RSAPrivateKeyWithSerialization) -> str:\n    return key.private_bytes(\n        encoding=serialization.Encoding.PEM,\n        format=serialization.PrivateFormat.PKCS8,\n        encryption_algorithm=serialization.NoEncryption()\n    )\n\n\ndef to_pem(certificate: x509.Certificate) -> str:\n    \"\"\"Convert an instance of x509.Certificate to a PEM string\n    \n    Args:\n          certificate (x509.Certificate): Cert to convert\n    Returns:\n          PEM string\n    \"\"\"\n    serialized = certificate.public_bytes(\n        encoding=serialization.Encoding.PEM\n    )\n\n    return serialized\n\n\ndef to_der(certificate: x509.Certificate) -> bytes:\n    \"\"\"Convert an instance of x509.Certificate to DER bytes\n    \n    Args:\n          certificate (x509.Certificate): Cert to convert\n    Returns:\n          DER bytes    \n    \"\"\"\n    serialized = certificate.public_bytes(\n        encoding=serialization.Encoding.DER,\n        format=serialization.PublicFormat.PKCS8,\n        encryption_algorithm=serialization.NoEncryption()\n    )\n\n    return serialized\n"
  },
  {
    "path": "commandment/pki/ssl.py",
    "content": "import datetime\nfrom typing import Optional\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography import x509\nfrom cryptography.x509 import NameOID, DNSName\n\n\ndef generate_signing_request(cn: str, dnsname: Optional[str] = None) -> (rsa.RSAPrivateKey, x509.CertificateSigningRequest):\n    \"\"\"Generate a Private Key + Certificate Signing Request using the given dnsname as the CN and SAN dNSName.\n    \n    Args:\n            cn (str): The certificate common name\n          dnsname (str): The public facing dns name of the MDM server.\n    Returns:\n          Tuple of rsa private key, csr\n    \"\"\"\n    private_key = rsa.generate_private_key(\n        public_exponent=65537,\n        key_size=2048,\n        backend=default_backend(),\n    )\n\n    name = x509.Name([\n        x509.NameAttribute(NameOID.COMMON_NAME, cn),\n        x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'commandment')\n    ])\n\n    builder = x509.CertificateSigningRequestBuilder()\n    builder = builder.subject_name(name)\n\n    if dnsname is not None:\n        san = x509.SubjectAlternativeName([\n            x509.DNSName(dnsname)\n        ])\n        builder = builder.add_extension(san, critical=True)\n\n    builder = builder.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)\n    \n    request = builder.sign(\n        private_key,\n        hashes.SHA256(),\n        default_backend()\n    )\n\n    return private_key, request\n\n\ndef generate_self_signed_certificate(cn: str) -> (rsa.RSAPrivateKey, x509.Certificate):\n    \"\"\"Generate an X.509 Certificate with the given Common Name.\n    \n    Args:\n          cn (string): \n    \"\"\"\n    name = x509.Name([\n        x509.NameAttribute(NameOID.COMMON_NAME, cn),\n        x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'commandment')\n    ])\n\n    private_key = rsa.generate_private_key(\n        public_exponent=65537,\n        key_size=2048,\n        backend=default_backend(),\n    )\n\n    certificate = x509.CertificateBuilder().subject_name(\n        name\n    ).issuer_name(\n        name\n    ).public_key(\n        private_key.public_key()\n    ).serial_number(\n        x509.random_serial_number()\n    ).not_valid_before(\n        datetime.datetime.utcnow()\n    ).not_valid_after(\n        datetime.datetime.utcnow() + datetime.timedelta(days=365)\n    ).add_extension(\n        x509.SubjectAlternativeName([\n            DNSName(cn)\n        ]), False\n    ).sign(private_key, hashes.SHA256(), default_backend())\n\n    return private_key, certificate\n"
  },
  {
    "path": "commandment/plistutil/__init__.py",
    "content": ""
  },
  {
    "path": "commandment/plistutil/nonewriter.py",
    "content": "from io import BytesIO\nfrom plistlib import _PlistWriter, FMT_XML, _FORMATS\n\n\nclass PlistNoneWriter(_PlistWriter):\n    \"\"\"This subclass of PlistWriter writes out XML formatted property lists, but specifically skips any key for which\n    its value is **None**.\"\"\"\n\n    def write_dict(self, d):\n        if d:\n            self.begin_element(\"dict\")\n            if self._sort_keys:\n                items = sorted(d.items())\n            else:\n                items = d.items()\n\n            for key, value in items:\n                if not isinstance(key, str):\n                    if self._skipkeys:\n                        continue\n                    raise TypeError(\"keys must be strings\")\n\n                if value is None:\n                    continue\n                    \n                self.simple_element(\"key\", key)\n                self.write_value(value)\n            self.end_element(\"dict\")\n\n        else:\n            self.simple_element(\"dict\")\n\n\n# These methods are copied here for convenience\n\ndef dump(value, fp, *, fmt=FMT_XML, sort_keys=True, skipkeys=False):\n    \"\"\"Write 'value' to a .plist file. 'fp' should be a (writable)\n    file object.\n    \"\"\"\n    if fmt not in _FORMATS:\n        raise ValueError(\"Unsupported format: %r\"%(fmt,))\n\n    writer = PlistNoneWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys)\n    writer.write(value)\n\n\ndef dumps(value, *, fmt=FMT_XML, skipkeys=False, sort_keys=True):\n    \"\"\"Return a bytes object with the contents for a .plist file.\n    \"\"\"\n    fp = BytesIO()\n    dump(value, fp, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys)\n    return fp.getvalue()\n"
  },
  {
    "path": "commandment/profiles/__init__.py",
    "content": "\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2017 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\"\"\"\nfrom enum import Enum\n\nPROFILE_CONTENT_TYPE = 'application/x-apple-aspen-config'\n\n\nclass PayloadScope(Enum):\n    User = 'User'\n    System = 'System'\n\n"
  },
  {
    "path": "commandment/profiles/ad.py",
    "content": "from typing import Set\nfrom enum import Enum, Flag, auto\n\n\nclass ADMountStyle(Enum):\n    AFP = 'afp'\n    SMB = 'smb'\n\n\nclass ADNamespace(Enum):\n    Domain = 'domain'\n    Forest = 'forest'\n\n\nclass ADOption(Flag):\n    CreateMobileAccountAtLogin = auto()\n    WarnUserBeforeCreatingMobileAccount = auto()\n    ForceHomeLocal = auto()\n    UseWindowsUNCPath = auto()\n    AllowMultiDomainAuth = auto()\n\nADOptions = Set[ADOption]\n\n\nclass ADPacketSignPolicy(Enum):\n    Allow = 'allow'\n    Disable = 'disable'\n    Require = 'require'\n\n\nclass ADPacketEncryptPolicy(Enum):\n    Allow = 'allow'\n    Disable = 'disable'\n    Require = 'require'\n    SSL = 'ssl'\n\n\nclass ADCertificateAcquisitionMechanism(Enum):\n    RPC = 'RPC'\n    HTTP = 'HTTP'\n"
  },
  {
    "path": "commandment/profiles/api.py",
    "content": "from flask import Blueprint\nfrom flask_rest_jsonapi import Api\nfrom commandment.profiles.resources import ProfilesList, ProfileDetail, ProfileRelationship\n\nprofiles_api_app = Blueprint('profiles_api', __name__)\napi = Api(blueprint=profiles_api_app)\n\n# Profiles (Different to profiles returned by inventory)\napi.route(ProfilesList, 'profiles_list', '/v1/profiles')\napi.route(ProfileDetail, 'profile_detail', '/v1/profiles/<int:profile_id>')\napi.route(ProfileRelationship, 'profile_tags', '/v1/profiles/<int:profile_id>/relationships/tags')\n# api.route(PayloadsList, 'payloads_list', '/v1/payloads')\n# api.route(PayloadDetail, 'payload_detail', '/v1/payloads/<int:payload_id>')\n"
  },
  {
    "path": "commandment/profiles/certificates.py",
    "content": "\"\"\"\nCopyright (c) 2015 Jesse Peterson, 2017 Mosen\nLicensed under the MIT license. See the included LICENSE.txt file for details.\n\"\"\"\n\nfrom enum import IntFlag\nfrom marshmallow import Schema, fields, post_load, post_dump\n\n\nclass KeyUsage(IntFlag):\n    \"\"\"Intended key usage flag. Used in SCEP payload.\"\"\"\n    Signing = 1\n    Encryption = 4\n    All = Signing | Encryption\n"
  },
  {
    "path": "commandment/profiles/eap.py",
    "content": "from typing import Set\nfrom enum import Enum, IntEnum\n\n\nclass EAPTypes(IntEnum):\n    \"\"\"EAP Types accepted by the EAPClient.\n    \n    See Also:\n          EAP8021X, EAP.h:51\n    \"\"\"\n    Invalid = 0\n    Identity = 1\n    Notification = 2\n    Nak = 3\n    MD5Challenge = 4\n    OneTimePassword = 5\n    GenericTokenCard = 6\n    TLS = 13\n    CiscoLEAP = 17\n    EAP_SIM = 18\n    SRP_SHA1 = 19\n    TTLS = 21\n    EAP_AKA = 23\n    PEAP = 25\n    MSCHAPv2 = 26\n    Extensions = 33\n    EAP_FAST = 43\n    EAP_AKA_Prime = 50\n\nAcceptEAPTypes = Set[EAPTypes]\n\n\nclass TTLSInnerAuthentication(Enum):\n    PAP = 'PAP'\n    CHAP = 'CHAP'\n    MSCHAP = 'MSCHAP'\n    MSCHAPv2 = 'MSCHAPv2'\n    EAP = 'EAP'\n"
  },
  {
    "path": "commandment/profiles/email.py",
    "content": "from enum import Enum\n\nclass EmailAccountType(Enum):\n    POP = 'EmailTypePOP'\n    IMAP = 'EmailTypeIMAP'\n\n\nclass EmailAuthenticationType(Enum):\n    Password = 'EmailAuthPassword'\n    CRAM_MD5 = 'EmailAuthCRAMMD5'\n    NTLM = 'EmailAuthNTLM'\n    HTTP_MD5 = 'EmailAuthHTTPMD5'\n    ENone = 'EmailAuthNone'\n"
  },
  {
    "path": "commandment/profiles/energy.py",
    "content": "from enum import Enum, IntFlag, auto\n\n\nclass ScheduledPowerEventType(Enum):\n    wake = 'wake'\n    wakepoweron = 'wakepoweron'\n    sleep = 'sleep'\n    shutdown = 'shutdown'\n    restart = 'restart'\n\n\nclass ScheduledPowerEventWeekdays(IntFlag):\n    def _generate_next_value_(name, start, count, last_values):\n        return 2 ** count\n\n    Monday = auto()\n    Tuesday = auto()\n    Wednesday = auto()\n    Thursday = auto()\n    Friday = auto()\n    Saturday = auto()\n    Sunday = auto()\n\n    All = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday\n"
  },
  {
    "path": "commandment/profiles/models.py",
    "content": "from commandment.profiles import PayloadScope\nfrom commandment.profiles.certificates import KeyUsage\nfrom ..dbtypes import GUID, JSONEncodedDict\nfrom uuid import uuid4\n\nfrom ..models import db\n\n\nclass Payload(db.Model):\n    __tablename__ = 'payloads'\n\n    id = db.Column(db.Integer, primary_key=True)\n    type = db.Column(db.String, index=True, nullable=False)\n    version = db.Column(db.Integer, default=1)\n    identifier = db.Column(db.String)\n    uuid = db.Column(GUID, index=True, default=uuid4(), nullable=False)\n    display_name = db.Column(db.String)\n    description = db.Column(db.Text)\n    organization = db.Column(db.String)\n\n    # Dependencies should be tracked in cases where the payload refers to another required payload.\n    # eg. a reference to certificate payload in an 802.1x configuration.\n    # depends_on = relationship(\"Payload\",\n    #                           secondary=payload_dependencies,\n    #                           backref=\"dependents\")\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'payload',\n        'polymorphic_on': type,\n    }\n\n\nclass MDMPayload(Payload):\n    id = db.Column(db.Integer, db.ForeignKey('payloads.id'), primary_key=True)\n    identity_certificate_uuid = db.Column(GUID, nullable=False)\n    topic = db.Column(db.String, nullable=False)\n    server_url = db.Column(db.String, nullable=False)\n    server_capabilities = db.Column(db.String)\n    sign_message = db.Column(db.Boolean)\n    check_in_url = db.Column(db.String)\n    check_out_when_removed = db.Column(db.Boolean)\n    access_rights = db.Column(db.Integer)\n    use_development_apns = db.Column(db.Boolean)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.mdm'\n    }\n\n\nclass SCEPPayload(Payload):\n    id = db.Column(db.Integer, db.ForeignKey('payloads.id'), primary_key=True)\n    url = db.Column(db.String, nullable=False)\n    name = db.Column(db.String, nullable=True)\n    subject = db.Column(JSONEncodedDict, nullable=False)  # eg. O=x/OU=y/CN=z\n    challenge = db.Column(db.String, nullable=True)\n    key_size = db.Column(db.Integer, default=2048, nullable=False)\n    ca_fingerprint = db.Column(db.LargeBinary, nullable=True)\n    key_type = db.Column(db.String, default='RSA', nullable=False)\n    key_usage = db.Column(db.Enum(KeyUsage), default=KeyUsage.All)\n    subject_alt_name = db.Column(db.String, nullable=True)\n    retries = db.Column(db.Integer, default=3, nullable=False)\n    retry_delay = db.Column(db.Integer, default=10, nullable=False)\n    certificate_renewal_time_interval = db.Column(db.Integer, default=14, nullable=False)\n\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.security.scep',\n    }\n\n\nclass CertificatePayload(Payload):\n    id = db.Column(db.Integer, db.ForeignKey('payloads.id'), primary_key=True)\n    certificate_file_name = db.Column(db.String)\n    payload_content = db.Column(db.LargeBinary)\n    password = db.Column(db.String)\n    __mapper_args__ = {\n        'polymorphic_identity': 'certificate'\n    }\n\n\nclass PEMCertificatePayload(CertificatePayload):\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.security.pem'\n    }\n\n\nclass DERCertificatePayload(CertificatePayload):\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.security.pkcs1'\n    }\n\n\nclass PKCS12CertificatePayload(CertificatePayload):\n    __mapper_args__ = {\n        'polymorphic_identity': 'com.apple.security.pkcs12'\n    }\n\n\nprofile_payloads = db.Table('profile_payloads', db.metadata,\n                            db.Column('profile_id', db.Integer, db.ForeignKey('profiles.id')),\n                            db.Column('payload_id', db.Integer, db.ForeignKey('payloads.id')))\n\n\nprofile_tags = db.Table('profile_tags', db.metadata,\n                    db.Column('profile_id',  db.Integer, db.ForeignKey('profiles.id')),\n                    db.Column('tag_id',  db.Integer, db.ForeignKey('tags.id')),\n                    )\n\n\nclass Profile(db.Model):\n    \"\"\"Top level profile.\n\n    See Also:\n          - `Configuration Profile Keys\n            <https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010206-CH1-SW7>`_.\n\n    Attributes:\n\n    \"\"\"\n    __tablename__ = 'profiles'\n\n    id = db.Column(db.Integer, primary_key=True)\n    data = db.Column(db.LargeBinary)\n\n    description = db.Column(db.Text)\n    display_name = db.Column(db.String)\n    expiration_date = db.Column(db.DateTime)  # Only for old style OTA\n    identifier = db.Column(db.String, nullable=False)\n    organization = db.Column(db.String)\n    uuid = db.Column(GUID, index=True, default=uuid4())\n    removal_disallowed = db.Column(db.Boolean)\n    version = db.Column(db.Integer, default=1)\n    payload_type = db.Column(db.String, default='Configuration')\n    scope = db.Column(db.Enum(PayloadScope), default=PayloadScope.User.value)\n    removal_date = db.Column(db.DateTime)\n    duration_until_removal = db.Column(db.BigInteger)\n    consent_en = db.Column(db.Text)\n    is_encrypted = db.Column(db.Boolean, default=False)\n\n    payloads = db.relationship('Payload',\n                               secondary=profile_payloads,\n                               backref='profiles')\n\n    tags = db.relationship('Tag',\n                           secondary=profile_tags,\n                           backref='profiles')\n\n"
  },
  {
    "path": "commandment/profiles/plist_schema.py",
    "content": "\"\"\"\nThis module defines marshmallow schemas for use in converting .mobileconfig (plist) representations into SQLAlchemy\nmodel representations.\n\"\"\"\n\nfrom typing import Union, Callable, Type, List, Dict\nfrom marshmallow import Schema, fields, post_load, post_dump\nfrom marshmallow_enum import EnumField\nfrom commandment.profiles import models\nfrom commandment.profiles.certificates import KeyUsage\nfrom . import PayloadScope\n\n_schemas: Dict[str, Schema] = {}\n\"\"\"Hold all registered schemas by their PayloadType.\"\"\"\n\n\ndef schema_for(payload_type: str) -> Union[None, Type[Schema]]:\n    \"\"\"Get a class that represents the marshmallow schema for a payload, using the payload type.\n    \n    Args:\n          payload_type (str): The value of PayloadType\n    Returns:\n          None or a class that represents a schema for that payload.\n    \"\"\"\n    return _schemas.get(payload_type, None)\n\n\ndef register_payload_schema(*args) -> Callable[[Type[Schema]], Type[Schema]]:\n    \"\"\"Decorate a Payload schema to register its type. For use with schema_for.\"\"\"\n    def wrapper(cls: Type[Schema]) -> Type[Schema]:\n        for payload_type in args:\n            _schemas[payload_type] = cls\n        return cls\n        \n    return wrapper\n\n\nclass Payload(Schema):\n    PayloadType = fields.Str(attribute='type', required=True)\n    PayloadVersion = fields.Integer(attribute='version', default=1)\n    PayloadIdentifier = fields.String(attribute='identifier')\n    PayloadUUID = fields.UUID(attribute='uuid')\n    PayloadDisplayName = fields.String(attribute='display_name')\n    PayloadDescription = fields.String(attribute='description')\n    PayloadOrganization = fields.String(attribute='organization')\n\n\n@register_payload_schema('Profile Service')\nclass ProfileServicePayload(Schema):\n    URL = fields.URL()\n    DeviceAttributes = fields.String(many=True)\n    Challenge = fields.String()\n    \n\nclass ConsentTextSchema(Schema):\n    en = fields.String(attribute='consent_en')\n\n\n@register_payload_schema('com.apple.security.pem', 'com.apple.security.root', 'com.apple.security.pkcs1',\n                         'com.apple.security.pkcs12')\nclass CertificatePayloadSchema(Payload):\n    PayloadCertificateFileName = fields.Str(attribute='certificate_file_name')\n    PayloadContent = fields.Raw(attribute='payload_content')\n    Password = fields.Str(attribute='password')\n\n\n\n@register_payload_schema('com.apple.security.scep')\nclass SCEPPayload(Payload):\n    URL = fields.URL(attribute='url')\n    Name = fields.String(attribute='name')\n    # Subject = fields.Nested()\n    Challenge = fields.String(attribute='challenge')\n    Keysize = fields.Integer(attribute='key_size')\n    CAFingerprint = fields.String(attribute='ca_fingerprint')\n    KeyType = fields.String(attribute='key_type')\n    KeyUsage = EnumField(KeyUsage, attribute='key_usage', by_value=True)\n    # SubjectAltName = fields.Dict(attribute='subject_alt_name')\n    Retries = fields.Integer(attribute='retries')\n    RetryDelay = fields.Integer(attribute='retry_delay')\n\n    @post_dump(pass_many=False)\n    def wrap_payload_content(self, data: dict) -> dict:\n        \"\"\"SCEP Payload is silly and double wraps its PayloadContent item.\"\"\"\n        inner_content = {\n            'URL': data.pop('URL', None),\n            'Name': data.pop('Name'),\n            'Challenge': data.pop('Challenge'),\n            'Keysize': data.pop('Keysize'),\n            'CAFingerprint': data.pop('CAFingerprint'),\n            'KeyType': data.pop('KeyType'),\n            'KeyUsage': data.pop('KeyUsage'),\n            'Retries': data.pop('Retries'),\n            'RetryDelay': data.pop('RetryDelay'),\n        }\n\n        data['PayloadContent'] = inner_content\n        return data\n\n    @post_load\n    def make_payload(self, data: dict) -> models.SCEPPayload:\n        return models.SCEPPayload(**data)\n\n\n@register_payload_schema('com.apple.mdm')\nclass MDMPayload(Payload):\n    IdentityCertificateUUID = fields.UUID(attribute='identity_certificate_uuid', required=True)\n    Topic = fields.String(attribute='topic', required=True)\n    ServerURL = fields.URL(attribute='server_url', required=True)\n    # ServerCapabilities = fields.Nested(many=True)\n    SignMessage = fields.Boolean(attribute='sign_message')\n    CheckInURL = fields.String(attribute='check_in_url')\n    CheckOutWhenRemoved = fields.Boolean(attribute='check_out_when_removed')\n    AccessRights = fields.Integer(attribute='access_rights')\n    UseDevelopmentAPNS = fields.Boolean(attribute='use_development_apns')\n\n    @post_load\n    def make_payload(self, data: dict) -> models.MDMPayload:\n        return models.MDMPayload(**data)\n\n\n\nclass ProfileSchema(Schema):\n    PayloadDescription = fields.Str(attribute='description')\n    PayloadDisplayName = fields.Str(attribute='display_name')\n    PayloadExpirationDate = fields.DateTime(attribute='expiration_date')\n    PayloadIdentifier = fields.Str(attribute='identifier', required=True)\n    PayloadOrganization = fields.Str(attribute='organization')\n    PayloadUUID = fields.UUID(attribute='uuid')\n    PayloadRemovalDisallowed = fields.Bool(attribute='removal_disallowed')\n    PayloadType = fields.Function(lambda obj: 'Configuration', attribute='payload_type')\n    PayloadVersion = fields.Function(lambda obj: 1, attribute='version')\n    PayloadScope = EnumField(PayloadScope, attribute='scope')\n    RemovalDate = fields.DateTime(attribute='removal_date')\n    DurationUntilRemoval = fields.Float(attribute='duration_until_removal')\n    ConsentText = fields.Nested(ConsentTextSchema())\n    PayloadContent = fields.Method('get_payloads', deserialize='load_payloads')\n\n    def get_payloads(self, obj):\n        payloads = []\n\n        for payload in obj.payloads:\n            schema = schema_for(payload.type)\n            if schema is not None:\n                result = schema().dump(payload)\n                payloads.append(result.data)\n            else:\n                print('Unsupported PayloadType: {}'.format(payload.type))\n\n        return payloads\n\n    def load_payloads(self, payload_content: list) -> List[Schema]:\n        payloads = []\n\n        for content in payload_content:\n            schema = schema_for(content['PayloadType'])\n            if schema is not None:\n                result = schema().load(content)\n                payloads.append(result.data)\n            else:\n                print('Unsupported PayloadType: {}'.format(content['PayloadType']))\n\n        return payloads\n\n\n    @post_load\n    def make_profile(self, data):\n        payloads = data.pop('PayloadContent', [])\n        p = models.Profile(**data)\n        # for pl in payloads:\n        #     p.payloads.append(pl)\n\n        return p"
  },
  {
    "path": "commandment/profiles/resources.py",
    "content": "from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship\nfrom commandment.models import db\nfrom commandment.profiles.models import Profile\nfrom commandment.profiles.schema import ProfileSchema\n\n\nclass ProfilesList(ResourceList):\n    schema = ProfileSchema\n    data_layer = {\n        'session': db.session,\n        'model': Profile\n    }\n\n\nclass ProfileDetail(ResourceDetail):\n    schema = ProfileSchema\n    data_layer = {\n        'session': db.session,\n        'model': Profile,\n        'url_field': 'profile_id'\n    }\n\n\nclass ProfileRelationship(ResourceRelationship):\n    schema = ProfileSchema\n    data_layer = {\n        'session': db.session,\n        'model': Profile,\n        'url_field': 'profile_id'\n    }\n"
  },
  {
    "path": "commandment/profiles/schema.py",
    "content": "from marshmallow_jsonapi import fields\nfrom marshmallow_jsonapi.flask import Relationship, Schema\nfrom marshmallow import Schema as FlatSchema, post_load\n\n\nclass ProfileSchema(Schema):\n    class Meta:\n        type_ = 'profiles'\n        self_view = 'profiles_api.profile_detail'\n        self_view_kwargs = {'profile_id': '<id>'}\n        self_view_many = 'profiles_api.profiles_list'\n\n    id = fields.Int(dump_only=True)\n    data = fields.String()\n\n    description = fields.Str()\n    display_name = fields.Str()\n    expiration_date = fields.DateTime()\n    identifier = fields.Str()\n    organization = fields.Str()\n    uuid = fields.UUID()\n    removal_disallowed = fields.Boolean()\n    version = fields.Int()\n    scope = fields.Str()\n    removal_date = fields.DateTime()\n    duration_until_removal = fields.Int()\n    consent_en = fields.Str()\n\n    tags = Relationship(\n        related_view='api_app.tag_detail',\n        related_view_kwargs={'tag_id': '<id>'},\n        many=True,\n        schema='TagSchema',\n        type_='tags'\n    )\n\n"
  },
  {
    "path": "commandment/profiles/vpn.py",
    "content": "from enum import Enum\n\nclass VPNType(Enum):\n    L2TP = 'L2TP'\n    PPTP = 'PPTP'\n    IPSec = 'IPSec'\n    IKEv2 = 'IKEv2'\n    AlwaysOn = 'AlwaysOn'\n    VPN = 'VPN'\n"
  },
  {
    "path": "commandment/profiles/wifi.py",
    "content": "from enum import Enum\n\n\nclass WIFIEncryptionType(Enum):\n    ENone = 'None'\n    Any = 'Any'\n    WPA2 = 'WPA2'\n    WPA = 'WPA'\n    WEP = 'WEP'\n\n\nclass WIFIProxyType(Enum):\n    ENone = 'None'\n    Manual = 'Manual'\n    Auto = 'Auto'\n\n"
  },
  {
    "path": "commandment/signals.py",
    "content": "from blinker import Namespace\nsignals = Namespace()\n\n# Sent when a device enrolls for the first time, or re-enrols after checking out\ndevice_enrolled = signals.signal('device-enrolled')\n\n# Sent when a device voluntarily checks out\ndevice_unenrolled = signals.signal('device-unenrolled')\n\n# Sent when a device checks in, including: Authenticate, TokenUpdate, Acknowledge, NotNow\ndevice_checkin = signals.signal('device-checkin')\n\n# If APNS tells us that a device token expired\ndevice_token_expired = signals.signal('device-token-expired')\n"
  },
  {
    "path": "commandment/static/.gitignore",
    "content": "app.js\n*.map\nfonts/*\ncss/*\nimages/*\n\n"
  },
  {
    "path": "commandment/static/index.dev.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>commandment</title>\n</head>\n<body>\n<div id=\"root\">\n</div>\n<script src=\"https://localhost:4000/static/app.js\" type=\"text/javascript\"></script>\n</body>\n</html>"
  },
  {
    "path": "commandment/static/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"stylesheet\" href=\"/static/css/app.css\">\n    <title>commandment</title>\n</head>\n<body>\n<div id=\"root\">\n</div>\n<script src=\"/static/app.js\" type=\"text/javascript\"></script>\n</body>\n</html>"
  },
  {
    "path": "commandment/storage/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "commandment/templates/index.html",
    "content": "{# The URL that assets will be loaded from if running from webpack-dev-server #}\n{% set webpack_dev_url = 'https://localhost:4000' %}\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    {% block head %}\n    <link href=\"https://fonts.googleapis.com/css?family=Roboto\" rel=\"stylesheet\">\n        {% if config['DEBUG'] == False %}\n            <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/app.css') }}\">\n        {% endif %}\n        <title>commandment</title>\n    {% endblock %}\n</head>\n<body>\n    <div id=\"root\">\n        loading {{ config['DEBUG'] }}\n    </div>\n    <script src=\"{{ webpack_dev_url if config['DEBUG'] else '' }}/static/app.js\" type=\"text/javascript\"></script>\n</body>\n</html>"
  },
  {
    "path": "commandment/threads/__init__.py",
    "content": ""
  },
  {
    "path": "commandment/threads/startup_thread.py",
    "content": "\"\"\"\nThis thread should run delayed, once at startup to initialise the internal CA and self-signed certificates to provide\na baseline configuration for messing around with.\n\"\"\"\n\nimport threading\nimport logging\nimport datetime\nimport os\nfrom flask_alembic import Alembic\nfrom oscrypto.keys import parse_pkcs12\nfrom commandment.models import db\nfrom commandment.pki.models import RSAPrivateKey, CertificateSigningRequest, CACertificate\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives import serialization, hashes\nfrom cryptography import x509\nfrom cryptography.x509.oid import NameOID\nimport sqlalchemy\nfrom commandment.pki.ca import get_ca\nfrom flask import Flask\n\nstartup_thread = None\nstartup_delay = 1.0\n\nlogger = logging.getLogger('startup thread')\n\n\ndef generate_ca(app: Flask):\n    \"\"\"Generate internal CA certificate for sandbox setups.\"\"\"\n    with app.app_context():\n        app.logger.info('Generating Internal CA if necessary...')\n        ca = get_ca()  # Implicit creation of `certificate_authority` row and certificates\n\n\ndef split_pkcs12(app: Flask):\n    \"\"\"Split up .p12 containers if necessary.\"\"\"\n    with app.app_context():\n        if 'PUSH_CERTIFICATE' not in app.config:\n            app.logger.warn('No push certificate specified, you will not be able to manage devices until this is configured')\n            return\n\n        push_certificate_path = app.config['PUSH_CERTIFICATE']\n        if not os.path.exists(push_certificate_path):\n            raise RuntimeError('You specified a push certificate at: {}, but it does not exist.'.format(push_certificate_path))\n\n        # We can handle loading PKCS#12 but APNS2Client specifically requests PEM encoded certificates\n        push_certificate_basename, ext = os.path.splitext(push_certificate_path)\n        if ext.lower() == '.p12':\n            pem_key_path = push_certificate_basename + '.key'\n            pem_certificate_path = push_certificate_basename + '.crt'\n\n            if not os.path.exists(pem_key_path) or not os.path.exists(pem_certificate_path):\n                app.logger.info('You provided a PKCS#12 push certificate, we will have to encode it as PEM to continue...')\n                app.logger.info('.key and .crt files will be saved in the same location: %s, %s', pem_key_path, pem_certificate_path)\n                with open(push_certificate_path, 'rb') as fd:\n                    if 'PUSH_CERTIFICATE_PASSWORD' in app.config:\n                        key, certificate, intermediates = parse_pkcs12(fd.read(), bytes(app.config['PUSH_CERTIFICATE_PASSWORD'], 'utf8'))\n                    else:\n                        key, certificate, intermediates = parse_pkcs12(fd.read())\n\n                try:\n                    crypto_key = serialization.load_der_private_key(key.dump(), None, default_backend())\n                    with open(pem_key_path, 'wb') as fd:\n                        fd.write(crypto_key.private_bytes(\n                            encoding=serialization.Encoding.PEM,\n                            format=serialization.PrivateFormat.PKCS8,\n                            encryption_algorithm=serialization.NoEncryption()))\n\n                    crypto_cert = x509.load_der_x509_certificate(certificate.dump(), default_backend())\n                    with open(pem_certificate_path, 'wb') as fd:\n                        fd.write(crypto_cert.public_bytes(serialization.Encoding.PEM))\n                except PermissionError:\n                    app.logger.error('Could not write out .key or .crt file. You will not be able to push APNS messages')\n                    app.logger.error('This means your MDM is BROKEN until you fix permissions')\n            else:\n                app.logger.info('.p12 already split into PEM/KEY components')\n\n\ndef run_migrations(app: Flask):\n    \"\"\"Run the database migrations.\"\"\"\n    with app.app_context():\n        app.logger.info('Running Alembic Migrations')\n        alembic = Alembic()\n        alembic.init_app(app, run_mkdir=False)\n        alembic.upgrade('head')\n\n\ndef startup_callback(app: Flask):\n    \"\"\"Run the StartUp Thread jobs\"\"\"\n    logger.debug(\"Started Thread: Startup\")\n    split_pkcs12(app)\n    run_migrations(app)\n    generate_ca(app)\n\n\ndef start(app: Flask):\n    \"\"\"Start the StartUp thread\"\"\"\n    logger.info('Startup thread will run in 5 seconds')\n    startup_thread = threading.Timer(startup_delay, startup_callback, [app])\n    startup_thread.daemon = True\n    startup_thread.start()\n"
  },
  {
    "path": "commandment/threads/vpp_thread.py",
    "content": "\"\"\"\nThis thread should synchronise available licenses\n\"\"\""
  },
  {
    "path": "commandment/utils.py",
    "content": "from flask import current_app\nimport plistlib\n\n\ndef plistify(*args, **kwargs):\n    \"\"\"Similar to jsonify, which ships with Flask, this function wraps plistlib.dumps and sets up the correct\n    mime type for the response.\"\"\"\n    if args and kwargs:\n        raise TypeError('plistify() behavior undefined when passed both args and kwargs')\n    elif len(args) == 1:  # single args are passed directly to dumps()\n        data = args[0]\n    else:\n        data = args or kwargs\n\n    mimetype = kwargs.get('mimetype', current_app.config['PLISTIFY_MIMETYPE'])\n\n    return current_app.response_class(\n        (plistlib.dumps(data), '\\n'),\n        mimetype=mimetype\n    )\n"
  },
  {
    "path": "commandment/vpp/__init__.py",
    "content": "from flask import g, current_app\n\nfrom commandment.vpp.errors import VPPError\nfrom commandment.vpp.vpp import VPP\n\n\ndef get_vpp() -> VPP:\n    vpp = getattr(g, '_vpp', None)\n\n    if vpp is None:\n        if 'VPP_STOKEN' not in current_app.config:\n            raise VPPError('VPP stoken not configured')\n\n        g._vpp = VPP(current_app.config['VPP_STOKEN'])\n\n    return vpp\n"
  },
  {
    "path": "commandment/vpp/app.py",
    "content": "from flask import Blueprint, jsonify, g, current_app, request, abort\nfrom flask_rest_jsonapi import Api\nfrom commandment.vpp.models import db, VPPAccount\nfrom commandment.vpp.schema import VPPAccountSchema\n\nvpp_app = Blueprint('vpp_app', __name__)\napi = Api(blueprint=vpp_app)\n\n\n@vpp_app.route('/api/v1/vpp/token', methods=['GET'])\ndef token():\n    \"\"\"Retrieve information about the current VPP token.\n\n    :resheader Content-Type: application/json\n    :statuscode 200:\n    :statuscode 404: No VPP token has been uploaded\n    \"\"\"\n    account = db.session.query(VPPAccount).first()\n    schema = VPPAccountSchema()\n    result = schema.dumps(account)\n    if result.errors:\n        abort(500)\n    else:\n        return result.data, 200, {'Content-Type': 'application/json'}\n\n\n@vpp_app.route('/api/v1/vpp/upload/token', methods=['POST'])\ndef upload_token():\n    \"\"\"Upload the VPP service token in the format normally issued by vpp.itunes.apple.com.\n\n    :reqheader Accept: application/octet-stream\n    :resheader Content-Type: application/json\n    :statuscode 201: VPP token stored successfully.\n    :statuscode 400: The request did not contain a valid VPP token.\n\n    \"\"\"\n    if 'file' not in request.files:\n        abort(400, 'no file uploaded in request data')\n\n    f = request.files['file']\n\n    if not f.content_type == 'application/octet-stream':\n        abort(400, 'incorrect MIME type in request')\n\n    data = f.read()\n    account = VPPAccount(stoken=data)\n    db.session.add(account)\n    db.session.commit()\n\n    return '{}', 201, {'Content-Type': 'application/json'}\n\n"
  },
  {
    "path": "commandment/vpp/cli.py",
    "content": ""
  },
  {
    "path": "commandment/vpp/decorators.py",
    "content": "import functools\n\nfrom commandment.vpp.errors import VPPAPIError\n\n\ndef raise_error_replies(f):\n    \"\"\"Decorator which wraps a function that returns the dict representing a direct response body from the VPP service.\n\n    The reply is checked for VPP errors and, if there are any errors, the error is raised as a VPPAPIError exception.\n    \"\"\"\n    @functools.wraps(f)\n    def wrapper(*args, **kwargs):\n        reply = f(*args, **kwargs)\n        if reply['status'] == -1:  # VPP Error occurred\n            raise VPPAPIError(reply['errorNumber'], reply['errorMessage'])\n        return reply\n\n    return wrapper\n"
  },
  {
    "path": "commandment/vpp/enum.py",
    "content": "from typing import Tuple\nfrom enum import Enum, IntEnum\n\n\nclass VPPPricingParam(Enum):\n    \"\"\"Valid values for the VPP pricingParam argument.\"\"\"\n    \n    StandardQuality = 'STDQ'\n    \"\"\"str: Standard Quality\"\"\"\n    HighQuality = 'PLUS'\n    \"\"\"str: High Quality - Does not apply to Software\"\"\"\n\n\nclass VPPUserStatus(Enum):\n    \"\"\"Valid values for the status of a VPP registered user.\"\"\"\n\n    Registered = 'Registered'\n    \"\"\"str: Registered\"\"\"\n    Associated = 'Associated'\n    \"\"\"str: Associated\"\"\"\n    Retired = 'Retired'\n    \"\"\"str: Retired (can still be changed back)\"\"\"\n    Deleted = 'Deleted'\n    \"\"\"str: Deleted\"\"\"\n\n\nAdamID = str\nPricingParam = str\nVPPAsset = Tuple[AdamID, PricingParam]\n\"\"\"VPPAsset: A tuple representing a pair of product adam id and pricing parameter.\"\"\"\n\nclass LicenseAssociationType(Enum):\n    \"\"\"Valid types of license association operations which are mutually exclusive in a single batch.\"\"\"\n\n    ClientUserID = 'ClientUserID'\n    \"\"\"str: Associate user to license by Client ID\"\"\"\n    SerialNumber = 'SerialNumber'\n    \"\"\"str: Associate device to license by Serial Number\"\"\"\n\nLicenseAssociation = Tuple[LicenseAssociationType, AdamID]\n\"\"\"LicenseAssociation: A tuple representing a combination of a product by adam id and a type of association operation\"\"\"\n\nclass LicenseDisassociationType(Enum):\n    \"\"\"Valid types of license disassociation operations which are mutually exclusive in a single batch.\"\"\"\n\n    ClientUserID = 'ClientUserID'\n    \"\"\"str: Disassociate license from user by Client ID\"\"\"\n    SerialNumber = 'SerialNumber'\n    \"\"\"str: Disassociate license from device by Serial Number\"\"\"\n    LicenseID = 'LicenseID'\n    \"\"\"str: Disassociate license by ID regardless of User/Device\"\"\"\n\nLicenseDisassociation = Tuple[LicenseDisassociationType, AdamID]\n\"\"\"LicenseDisassociation: A tuple representing a combination of a product by adam id and a type of \n    disassociation operation\"\"\"\n\n\nclass VPPProductType(IntEnum):\n    \"\"\"A VPP product type. Required by some of the VPP API\"\"\"\n    Software = 7\n    \"\"\"int: An piece of software\"\"\"\n    Application = 8\n    \"\"\"int: Don't ask me\"\"\"\n    Publication = 10\n    \"\"\"int: An ebook\"\"\"\n"
  },
  {
    "path": "commandment/vpp/errors.py",
    "content": "from enum import IntEnum\n\n\nclass VPPErrorType(IntEnum):\n    \"\"\"An enumeration representation of all (currently) possible error codes returned by the VPP API.\"\"\"\n    \n    MissingArgument = 9600\n    LoginRequired = 9601\n    InvalidArgument = 9602\n    InternalError = 9603\n    ResultNotFound = 9604\n    AccountStorefrontIncorrect = 9605\n    ErrorConstructingToken = 9606\n    LicenseIrrevocable = 9607\n    EmptyResponseFromSharedData = 9608\n    UserNotFound = 9609\n    LicenseNotFound = 9610\n    AdminNotFound = 9611\n    FailedCreatingClaimJob = 9612\n    FailedCreatingUnclaimJob = 9613\n    InvalidDateFormat = 9614\n    OrgCountryNotFound = 9615\n    LicenseAlreadyAssigned = 9616\n    UserAlreadyRetired = 9618\n    LicenseNotAssociated = 9619\n    UserAlreadyDeleted = 9620\n    TokenExpired = 9621\n    InvalidAuthenticationToken = 9622\n    InvalidAPNSToken = 9623\n    LicenseRefunded = 9624\n    STokenRevoked = 9625\n    LicenseAlreadyAssignedUser = 9626\n    DeviceAssignmentNotAllowed = 9628\n    TooManyAssignmentErrors = 9630\n    TooManyNoLicenseErrors = 9631\n    TooManyDuplicateAssignments = 9632\n    DataBatchUnrecoverable = 9633\n    Deprecated = 9634\n    AppleIDInvalid = 9635\n    RegisteredUserNotFound = 9636\n    STokenPermissionDenied = 9637\n    FacilitatorHasNoManagedID = 9638\n    FacilitatorMemberIDNotFound = 9639\n    FacilitatorDetailsNotAvailable = 9640\n    \n\nclass VPPError(Exception):\n    \"\"\"Generic error used when the service returns an error of any kind\"\"\"\n    pass\n\n\nclass VPPAPIError(VPPError):\n    \"\"\"If the VPP API returns an error code, it is raised using this error class.\n\n    Attributes:\n          errno (int): The errorNumber\n          message (str): The error message\n    \"\"\"\n    def __init__(self, errno, message):\n        self.errno = errno\n        self.message = message\n"
  },
  {
    "path": "commandment/vpp/models.py",
    "content": "from ..dbtypes import GUID, JSONEncodedDict\nfrom .enum import VPPUserStatus, VPPPricingParam, VPPProductType\n\nfrom ..models import db\nfrom sqlalchemy.ext.hybrid import hybrid_property, hybrid_method\nimport base64\nimport json\nimport dateutil.parser\n\n\nclass VPPAccount(db.Model):\n    __tablename__ = 'vpp_accounts'\n\n    id = db.Column(db.Integer, primary_key=True)\n\n    @hybrid_property\n    def stoken(self) -> str:\n        return self._stoken\n\n    @stoken.setter\n    def stoken(self, value: str):\n        self._stoken = value\n        decoded = base64.b64decode(value)\n        data = json.loads(decoded)\n        self.exp_date = dateutil.parser.parse(data['expDate'])\n        self.org_name = data['orgName']\n\n    _stoken = db.Column(db.String, nullable=False)\n    exp_date = db.Column(db.DateTime)\n    \"\"\"datetime: Populated for convenience when checking the VPP token expiry date.\"\"\"\n    org_name = db.Column(db.String)\n    \"\"\"string: Populated for convenience.\"\"\"\n\n    # at least one of these must be null at all times\n    licenses_since_modified_token = db.Column(db.String)\n    licenses_batch_token = db.Column(db.String)\n\n    users_since_modified_token = db.Column(db.String)\n    users_batch_token = db.Column(db.String)\n\n    # ASM/ABM Location Information\n    location_id = db.Column(db.Integer)\n    location_name = db.Column(db.String)\n\n\nclass VPPUser(db.Model):\n    __tablename__ = 'vpp_users'\n\n    user_id = db.Column(db.Integer, primary_key=True)\n    client_user_id = db.Column(GUID, nullable=False)\n    email = db.Column(db.String)\n    status = db.Column(db.Enum(VPPUserStatus))\n    invite_url = db.Column(db.String)\n    invite_code = db.Column(db.String)\n\n\nclass VPPLicense(db.Model):\n    __tablename__ = 'vpp_licenses'\n\n    license_id = db.Column(db.Integer, primary_key=True)\n    adam_id = db.Column(db.String)\n    product_type = db.Column(db.Enum(VPPProductType))\n    product_type_name = db.Column(db.String)\n    pricing_param = db.Column(db.Enum(VPPPricingParam))\n    is_irrevocable = db.Column(db.Boolean)\n    user_id = db.Column(db.ForeignKey('vpp_users.user_id'))\n    client_user_id = db.Column(db.ForeignKey('vpp_users.client_user_id'))\n    its_id_hash = db.Column(db.String)\n    \n\n"
  },
  {
    "path": "commandment/vpp/schema.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass VPPAccountSchema(Schema):\n    exp_date = fields.DateTime()\n    org_name = fields.String()\n"
  },
  {
    "path": "commandment/vpp/vpp.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nVolume Purchase Programme Support\n\nTODO:\n    - Not all error conditions are unit tested.\n    - Only the license cursor has been tested.\n    - The license assignment method remains untested.\n\"\"\"\n\nimport requests\nfrom typing import List, Optional, Iterator, Tuple, Dict, Text, Any\nimport json\nimport base64\n\nfrom commandment.vpp.decorators import raise_error_replies\nfrom commandment.vpp.enum import LicenseAssociation, LicenseDisassociation, LicenseAssociationType, \\\n    LicenseDisassociationType, VPPPricingParam\n\nSERVICE_CONFIG_URL = 'https://vpp.itunes.apple.com/WebObjects/MZFinance.woa/wa/VPPServiceConfigSrv'\n\"\"\"str: The default production URL to fetch VPP service configuration from.\"\"\"\n\n\ndef encode_stoken(token: dict) -> bytes:\n    \"\"\"Encode a dict containing the sToken properties into a base64 token for use with VPP.\n\n    Args:\n          token (dict): Token containing the 'token', 'expDate', and 'orgName' fields.\n\n    Returns:\n          bytes: Base64 encoded token.\n    \"\"\"\n    return base64.urlsafe_b64encode(json.dumps(token, separators=(',', ':')).encode('utf8'))\n\n\nclass VPPCursor(object):\n    \"\"\"Generic base class for operations on endpoints that require a token to retrieve multiple pages of records.\n\n    Attributes:\n          _current (dict): Current results.\n          _vpp (VPP): Instance of the VPP object that generated this cursor.\n    \"\"\"\n\n    @property\n    def batch_count(self) -> Optional[int]:\n        \"\"\"Optional[int]: Number of records returned in this batch.\"\"\"\n        return self._current.get('batchCount', None)\n\n    @property\n    def total(self) -> Optional[int]:\n        \"\"\"Optional[int]: Number of records in total that will be returned.\"\"\"\n        return self._current.get('totalCount', None)\n\n    @property\n    def batch_token(self) -> Optional[str]:\n        \"\"\"Optional[str]: The batch token, if a batch fetch is in progress and is not complete.\"\"\"\n        return self._current.get('batchToken', None)\n\n    @property\n    def since_modified_token(self) -> Optional[str]:\n        \"\"\"Optional[str]: The since modified token, if a batch fetch is not in progress, but a fetch has been\n        made.\"\"\"\n        return self._current.get('sinceModifiedToken', None)\n\n    def __init__(self, since_modified_token: str = None, vpp=None) -> None:\n        self._current: Dict[Text, Any] = {}\n        if since_modified_token is not None:\n            self._current['sinceModifiedToken'] = since_modified_token\n\n        self._vpp = vpp\n\n\nclass VPPUserCursor(VPPCursor):\n    \"\"\"VPPUserCursor represents a batch fetch operation on the `getVPPUsersSrv` endpoint.\n\n    Attributes:\n          includes_retired (bool): This fetch operation includes users that have been marked as *Retired*.\n    \"\"\"\n\n    @property\n    def users(self) -> Optional[List[dict]]:\n        \"\"\"Optional[List[dict]]: The current set of users in the cursor result, or None if there are no results.\"\"\"\n        return self._current.get('users', None)\n\n    def __init__(self, includes_retired: bool = True, vpp=None) -> None:\n        super(VPPUserCursor, self).__init__(vpp=vpp)\n        self.includes_retired = includes_retired\n\n    def next(self):\n        \"\"\"\n\n        Returns:\n            next VPPUserCursor or None when batch is exhausted\n        \"\"\"\n        if self.batch_token is not None:\n            next_cursor = self._vpp.users(batch_token=self.batch_token)\n            next_cursor.includes_retired = self.includes_retired\n\n            return next_cursor\n        else:\n            return None\n\n\nclass VPPLicenseCursor(VPPCursor):\n    \"\"\"VPPLicenseCursor represents a batch fetch operation on the `getVPPLicensesSrv` endpoint.\n\n    \"\"\"\n\n    @property\n    def licenses(self) -> Optional[List[dict]]:\n        \"\"\"Optional[List[dict]]: The current set of licenses in the cursor result, or None if there are\n        no results.\"\"\"\n        return self._current.get('licenses', None)\n\n    def __init__(self, *args, **kwargs) -> None:\n        super(VPPLicenseCursor, self).__init__(*args, **kwargs)\n\n    def next(self):\n        \"\"\"\n\n        Returns:\n            next VPPLicenseCursor or None when batch is exhausted\n        \"\"\"\n        if self.batch_token is not None:\n            next_cursor = self._vpp.licenses(batch_token=self.batch_token)\n            self._current = next_cursor._current\n            return self\n        else:\n            return None\n\n\nclass VPPLicenseOperation(object):\n    \"\"\"VPPLicenseOperation represents a number of license operations on a single Adam ID (iTunes Store Product).\n\n    Attributes:\n          _association_type (LicenseAssociationType): This specifies the type of association this license operation\n            represents. The API only accepts one of these in a single request.\n          _disassociation_type (LicenseDisassociationType): This specifies the type of disassociation this license\n            operation represents. The API only accepts one of these in a single request.\n    \"\"\"\n    # _vpp: VPP\n\n    @property\n    def adam_id(self) -> int:\n        return self._adam_id\n\n    @property\n    def pricing_param(self) -> str:\n        return self._pricing_param\n\n    @property\n    def associations(self) -> Tuple[LicenseAssociationType, List[LicenseAssociation]]:\n        return self._association_type, self._associate\n\n    @property\n    def disassociations(self) -> Tuple[LicenseDisassociationType, List[LicenseDisassociation]]:\n        return self._disassociation_type, self._disassociate\n\n    def __init__(self, adam_id: int, pricing_param: str = 'STDQ',\n                 license_association_type: Optional[LicenseAssociationType] = None,\n                 license_disassociation_type: Optional[LicenseDisassociationType] = None) -> None:\n        self._adam_id = adam_id\n        self._pricing_param = pricing_param\n        self._associate: List[LicenseAssociation] = []\n        self._disassociate: List[LicenseDisassociation] = []\n        self._association_type = license_association_type\n        self._disassociation_type = license_disassociation_type\n\n    def add(self, association_type: LicenseAssociationType, value: str):\n        if self._association_type is None:\n            self._association_type = association_type\n        elif association_type != self._association_type:\n            raise ValueError('You cannot specify two different types of association in a license operation.')\n\n        self._associate.append((association_type, value))\n\n    def additions_for_type(self, association_type: LicenseAssociationType) -> Iterator[LicenseAssociation]:\n        return filter(lambda x: x[0] == association_type, self._associate)\n\n    def remove(self, disassociation_type: LicenseDisassociationType, value: str):\n        if self._disassociation_type is None:\n            self._disassociation_type = disassociation_type\n        elif disassociation_type != self._disassociation_type:\n            raise ValueError('You cannot specify two different types of disassociation in a license operation.')\n\n        self._disassociate.append((disassociation_type, value))\n\n    def removals_for_type(self, disassociation_type: LicenseDisassociationType) -> Iterator[LicenseDisassociation]:\n        return filter(lambda x: x[0] == disassociation_type, self._disassociate)\n\n\nclass VPPUserLicenseOperation(VPPLicenseOperation):\n    \"\"\"This object represents a batch operation on a license which will be associated to or disassociated from an\n    MDM user. AKA VPP User License Assignment.\n\n    Args:\n          adam_id (int): The Adam ID of the iTunes Store asset to manage.\n          pricing_param (str): The pricing parameter, defaults to 'STDQ' (Standard Quality)\n    \"\"\"\n\n    def __init__(self, *args, **kwargs) -> None:\n        super(VPPUserLicenseOperation, self).__init__(*args, **kwargs)\n        self._association_type = LicenseAssociationType.ClientUserID\n        self._disassociation_type = LicenseDisassociationType.ClientUserID\n\n\nclass VPPDeviceLicenseOperation(VPPLicenseOperation):\n    \"\"\"This object represents a batch operation on a license which will be associated to or disassociated from a\n    Device Serial Number. AKA VPP Device License Assignment.\n\n    Args:\n          adam_id (int): The Adam ID of the iTunes Store asset to manage.\n          pricing_param (str): The pricing parameter, defaults to 'STDQ' (Standard Quality)\n    \"\"\"\n\n    def __init__(self, *args, **kwargs) -> None:\n        super(VPPDeviceLicenseOperation, self).__init__(*args, **kwargs)\n        self._association_type = LicenseAssociationType.SerialNumber\n        self._disassociation_type = LicenseDisassociationType.SerialNumber\n\n\nclass VPP(object):\n    \"\"\"\n    VPP Object. The main VPP API wrapper class.\n\n    Attributes:\n        VPP.AssociationProperties (dict): Mapping of the LicenseAssociationType enum to the expected JSON keys in\n            the request.\n        VPP.DisassociationProperties (dict): Mapping of the LicenseDisassociationType enum to the expected JSON keys\n            in the request.\n    \"\"\"\n\n    AssociationProperties = {\n        LicenseAssociationType.ClientUserID: 'associateClientUserIdStrs',\n        LicenseAssociationType.SerialNumber: 'associateSerialNumbers'\n    }\n\n    DisassociationProperties = {\n        LicenseDisassociationType.SerialNumber: 'disassociateSerialNumbers',\n        LicenseDisassociationType.ClientUserID: 'disassociateClientUserIdStrs',\n        LicenseDisassociationType.LicenseID: 'disassociateLicenseIdStrs',\n    }\n\n    def __init__(self, stoken: str, vpp_service_config_url: str = SERVICE_CONFIG_URL, service_config: dict = None) -> None:\n        \"\"\"\n        The VPP class is a wrapper around a requests session and provides an API for interacting with Apple's VPP\n        service.\n\n        Args:\n            stoken (str): Service Token\n            vpp_service_config_url (str): URL to the VPPServiceConfigSrv endpoint. defaults to Apple's live server.\n            service_config (dict): Dictionary containing service config, if you do not want to fetch it (testing only).\n        \"\"\"\n        self._session = requests.Session()\n        self._session.headers.update({'Content-Type': 'application/json'})\n        self._stoken = stoken\n\n        if not service_config:\n            fetched_service_config = self._fetch_config(vpp_service_config_url)\n            self._service_config = fetched_service_config\n        else:\n            self._service_config = service_config\n\n    def _fetch_config(self, service_config_url: str) -> dict:\n        \"\"\"Fetch the service configuration from Apple, which contains all of the URLs required for VPP.\n\n        Args:\n            service_config_url (str): The VPPServiceConfigSrv URL to use\n        \"\"\"\n        res = self._session.get(service_config_url)\n        return res.json()\n\n    @raise_error_replies\n    def register_user(self, client_user_id: str, email: str = None, facilitator_member_id: str = None,\n                      managed_apple_id: str = None):\n        \"\"\"\n        Register an MDM user with VPP.\n\n        Args:\n            client_user_id (str): A unique string, usually a UUID to identify the user in the MDM.\n            email (str): The e-mail address of the user.\n            facilitator_member_id (str): Currently unused\n            managed_apple_id (str): Currently unused\n\n        Returns:\n            dict: Containing the decoded body of the reply from the VPP service, eg::\n\n                { \"status\": 0,\n                    \"user\": {\n                      \"userId\": 2878111686099947,\n                      \"email\": \"vpp-test@localhost\",\n                      \"status\": \"Registered\",\n                      \"inviteUrl\": \"http://localhost:8080/D1971F9DD5F8E67BDD\",\n                      \"inviteCode\": \"D1971F9DD5F8E67BDD\",\n                      \"clientUserIdStr\": \"F33D9E0F-CDE3-427E-A444-B137BEF9EFA2\"\n                    }\n                }\n        \"\"\"\n        res = self._session.post(self._service_config['registerUserSrvUrl'], data=json.dumps({\n            'clientUserIdStr': client_user_id,\n            'email': email,\n            'sToken': self._stoken,\n        }))\n        return res.json()\n\n    @raise_error_replies\n    def get_user(self, client_user_id: str = None, its_id_hash: str = None, facilitator_member_id: str = None,\n                 user_id: int = None):\n        \"\"\"\n        Get the status of a user by their unique ID.\n        \n        Args:\n            client_user_id (str): A unique string, usually a UUID to identify the user in the MDM. You can use this OR\n                the user_id to identify the user.\n            its_id_hash (str): (Optional) iTunes Store ID hash\n            facilitator_member_id:\n            user_id (int): User ID which uniquely identifies the user with the iTunes store.\n\n        Returns:\n            dict: Containing the reply from the service.\n        \"\"\"\n        request_body = {'sToken': self._stoken}\n        if user_id is not None:\n            request_body['userId'] = user_id\n        else:\n            request_body['clientUserIdStr'] = client_user_id\n            if its_id_hash is not None:\n                request_body['itsIdHash'] = its_id_hash\n\n        res = self._session.post(self._service_config['getUserSrvUrl'], data=json.dumps(request_body))\n        return res.json()\n\n    def users(self, include_retired: int = 1, facilitator_member_id: str = None,\n              batch_token: str = None, since_modified_token: str = None) -> VPPUserCursor:\n        \"\"\"\n\n        Args:\n            include_retired (int): 0 - do not include retired users, 1 - include retired users\n            facilitator_member_id: Currently unused\n            batch_token (str): Batch token (if being called from a cursor)\n            since_modified_token (str): Since modified token (if requesting a time delta)\n\n        Returns:\n\n        \"\"\"\n        request_body = {'sToken': self._stoken}\n        if include_retired == 1:\n            request_body['includeRetired'] = 1\n\n        if batch_token is not None:\n            request_body['batchToken'] = batch_token\n        elif since_modified_token is not None:\n            request_body['sinceModifiedToken'] = since_modified_token\n\n        res = self._session.post(self._service_config['getUsersSrvUrl'], data=json.dumps(request_body))\n        results = res.json()\n        cursor = VPPUserCursor(includes_retired=(include_retired == 1))\n        cursor._current = results\n        cursor._vpp = self\n\n        return cursor\n\n    @raise_error_replies\n    def retire_user(self, client_user_id: str = None, facilitator_member_id: str = None,\n                    user_id: str = None):\n        \"\"\"\n        Unregister a user from VPP.\n        \n        Args:\n            client_user_id (str): A unique string, usually a UUID to identify the user in the MDM. You can use this OR\n                the user_id to identify the user.\n            facilitator_member_id: Currently unused\n            user_id (int): User ID which uniquely identifies the user with the iTunes store.\n\n        Returns:\n            dict: Containing the reply from the service.\n        \"\"\"\n        request_body = {'sToken': self._stoken}\n        if user_id is not None:\n            request_body['userId'] = user_id\n        else:\n            request_body['clientUserIdStr'] = client_user_id\n\n        res = self._session.post(self._service_config['retireUserSrvUrl'], data=json.dumps(request_body))\n        return res.json()\n\n    @raise_error_replies\n    def edit_user(self, client_user_id: str = None, facilitator_member_id: str = None,\n                  email: str = None, managed_apple_id: str = None,\n                  user_id: str = None):\n        \"\"\"\n        Edit a user's VPP record.\n        \n        Args:\n            client_user_id (str): A unique string, usually a UUID to identify the user in the MDM. You can use this OR\n                the user_id to identify the user.\n            facilitator_member_id: Currently unused\n            email (str): Supply an E-mail address to update the current address.\n            user_id (int): User ID which uniquely identifies the user with the iTunes store.\n            managed_apple_id (str): Managed Apple ID\n\n        Returns:\n            dict: Containing the reply from the service.\n        \"\"\"\n        request_body = {'sToken': self._stoken}\n        if user_id is not None:\n            request_body['userId'] = user_id\n        else:\n            request_body['clientUserIdStr'] = client_user_id\n\n        if email is not None:\n            request_body['email'] = email\n\n        if managed_apple_id is not None:\n            request_body['managedAppleIDStr'] = managed_apple_id\n\n        res = self._session.post(self._service_config['editUserSrvUrl'], data=json.dumps(request_body))\n        return res.json()\n\n    @raise_error_replies\n    def assets(self, include_license_counts: bool = True, facilitator_member_id: str = None) -> List[dict]:\n        \"\"\"\n        Get assets for which the organization has licenses.\n        \n        Args:\n            include_license_counts (bool): Include counts of total/assigned/unassigned licenses.\n            facilitator_member_id: Currently unused\n\n        Returns:\n            List[dict]: List of VPP assets for which this organization has licenses.\n        \"\"\"\n        request_body = {\n            'sToken': self._stoken,\n            'includeLicenseCounts': include_license_counts,\n        }\n\n        res = self._session.post(self._service_config['getVPPAssetsSrvUrl'], data=json.dumps(request_body))\n        return res.json()\n\n    def manage(self, adam_id: int, pricing_param: str = 'STDQ') -> VPPLicenseOperation:\n        \"\"\"Manage VPP licenses for the given Adam ID.\n\n        Args:\n            adam_id (str): The Adam ID\n            pricing_param (str): The pricing param defaults to 'STDQ' but may be 'PLUS' for things which aren't\n            software.\n\n        Returns:\n            VPPLicenseOperation: an instance of a VPP license operation which can be modified to add or remove devices,\n            and then submitted.\n        \"\"\"\n        op = VPPLicenseOperation(adam_id, pricing_param)\n        op._vpp = self\n        return op\n\n    def manage_user_licenses(self, adam_id: int, pricing_param: str = 'STDQ') -> VPPUserLicenseOperation:\n        \"\"\"Manage VPP User License Assignment.\n\n        Args:\n            adam_id (str): The Adam ID\n            pricing_param (str): The pricing param defaults to 'STDQ' but may be 'PLUS' for things which aren't\n            software.\n\n        Returns:\n            VPPUserLicenseOperation: an instance of a VPP license operation which can be modified to add or remove license\n                associations by user client id\n            \"\"\"\n        op = VPPUserLicenseOperation(adam_id, pricing_param)\n        op._vpp = self\n        return op\n\n    def manage_device_licenses(self, adam_id: int, pricing_param: str = 'STDQ') -> VPPDeviceLicenseOperation:\n        \"\"\"Manage VPP Device License Assignment.\n\n        Args:\n            adam_id (str): The Adam ID\n            pricing_param (str): The pricing param defaults to 'STDQ' but may be 'PLUS' for things which aren't\n            software.\n\n        Returns:\n            VPPDeviceLicenseOperation: an instance of a VPP license operation which can be modified to add or remove\n                license associations by device serial number\n        \"\"\"\n        op = VPPDeviceLicenseOperation(adam_id, pricing_param)\n        op._vpp = self\n        return op\n\n    def licenses(self,\n                 adam_id: int = None,\n                 pricing_param: Optional[VPPPricingParam] = None,\n                 assigned_only: bool = False,\n                 facilitator_member_id: str = None,\n                 batch_token: str = None,\n                 since_modified_token: str = None) -> VPPLicenseCursor:\n        \"\"\"Retrieve a list of licenses matching the supplied criteria.\n\n        Args:\n              adam_id (int): Get licenses that match this Adam ID\n              pricing_param (Optional[VPPPricingParam]): Get licenses that match this 'Quality' param.\n              assigned_only (bool): Return only licenses that are assigned to users, if this value is true.\n              facilitator_member_id (str): Currently unused\n              batch_token (str): Supplied if there are more results to fetch.\n              since_modified_token (str): Supplied if you want to fetch results modified since a certain date. This will\n                be supplied on the last page of your most recent set of results.\n\n        Returns:\n              VPPLicenseCursor: A cursor that can be used to fetch all remaining results, pre-populated with the first\n                page.\n        \"\"\"\n        request_body = {'sToken': self._stoken}\n        if assigned_only:\n            request_body['assignedOnly'] = True\n        if batch_token:\n            request_body['batchToken'] = batch_token\n        if since_modified_token:\n            request_body['sinceModifiedToken'] = since_modified_token\n\n        # These parameters are normally ignored if a batch/modified token is supplied.\n        if batch_token is None and since_modified_token is None:\n            if adam_id is not None:\n                request_body['adamId'] = adam_id\n            if pricing_param is not None:\n                request_body['pricingParam'] = pricing_param.value\n\n        res = self._session.post(self._service_config['getLicensesSrvUrl'], data=json.dumps(request_body))\n        reply = res.json()\n        cursor = VPPLicenseCursor(vpp=self)\n        cursor._current = reply\n\n        return cursor\n\n    def save(self, operation: VPPLicenseOperation, notify: bool = False) -> dict:\n        \"\"\"Execute a license management operation, represented by a VPPLicenseOperation or subclass.\n\n        This provides a more convenient interface than bulk_update_licenses.\n\n        Args:\n              operation (VPPLicenseOperation): The license operation to perform.\n              notify (bool): Optional. Notify devices of license disassociation.\n        Returns:\n              dict: Reply from the license endpoint.\n        \"\"\"\n        atype, associations = operation.associations\n        dtype, disassociations = operation.disassociations\n\n        request_body = {\n            'sToken': self._stoken,\n            'adamIdStr': operation.adam_id,\n            'pricingParam': operation.pricing_param,\n            'notifyDisassociation': notify,\n            VPP.AssociationProperties[atype]: associations,\n            VPP.DisassociationProperties[dtype]: disassociations,\n        }\n\n        res = self._session.post(self._service_config['manageVPPLicensesByAdamIdSrvUrl'], data=json.dumps(request_body))\n        reply = res.json()\n\n        return reply\n\n    def bulk_update_licenses(self,\n                             adam_id: int,\n                             association_type: Optional[LicenseAssociationType] = None,\n                             associate: Optional[List[str]] = None,\n                             disassociation_type: Optional[LicenseDisassociationType] = None,\n                             disassociate: Optional[List[str]] = None,\n                             pricing_param: str = 'STDQ',\n                             notify: bool = False) -> dict:\n        \"\"\"Perform a batch operation of license associations and disassociations.\n\n        Args:\n              adam_id (int): Adam ID - The iTunes Store Product for which licenses will be managed.\n              association_type (Optional[LicenseAssociationType]): Provide an association type if associate length > 0\n              associate (Optional[List[str]]): A list of values that will be used to associate licenses, corresponding\n                to the association_type\n              disassociation_type (Optional[LicenseDisassociationType]): Provide a disassociation type if disassociate\n                length > 0.\n              disassociate (Optional[List[str]]): A list of values that will be used to disassociate licenses,\n                corresponding to the association_type\n              pricing_param (str): Defaults to Standard Quality 'STDQ'\n              notify (bool): Notify disassociation, default is False\n\n        See Also:\n            - manageVPPLicensesByAdamIdSrv\n        \"\"\"\n        request_body = {\n            'sToken': self._stoken,\n            'adamIdStr': adam_id,\n            'pricingParam': pricing_param,\n            'notifyDisassociation': notify,\n        }\n\n        if association_type in VPP.AssociationProperties:\n            request_body[VPP.AssociationProperties[association_type]] = associate\n\n        if disassociation_type in VPP.DisassociationProperties:\n            request_body[VPP.DisassociationProperties[disassociation_type]] = disassociate\n\n        res = self._session.post(self._service_config['manageVPPLicensesByAdamIdSrvUrl'], data=json.dumps(request_body))\n        reply = res.json()\n\n        return reply\n"
  },
  {
    "path": "doc/.gitignore",
    "content": "_build\n"
  },
  {
    "path": "doc/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  applehelp  to make an Apple Help Book\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  epub3      to make an epub3\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\t@echo \"  coverage   to run coverage check of the documentation (if enabled)\"\n\t@echo \"  dummy      to check syntax errors of document sources\"\n\n.PHONY: clean\nclean:\n\trm -rf $(BUILDDIR)/*\n\n.PHONY: html\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\n.PHONY: dirhtml\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\n.PHONY: singlehtml\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\n.PHONY: pickle\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\n.PHONY: json\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\n.PHONY: htmlhelp\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\n.PHONY: qthelp\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/commandment.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/commandment.qhc\"\n\n.PHONY: applehelp\napplehelp:\n\t$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp\n\t@echo\n\t@echo \"Build finished. The help book is in $(BUILDDIR)/applehelp.\"\n\t@echo \"N.B. You won't be able to view it unless you put it in\" \\\n\t      \"~/Library/Documentation/Help or install it in your application\" \\\n\t      \"bundle.\"\n\n.PHONY: devhelp\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/commandment\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/commandment\"\n\t@echo \"# devhelp\"\n\n.PHONY: epub\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\n.PHONY: epub3\nepub3:\n\t$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3\n\t@echo\n\t@echo \"Build finished. The epub3 file is in $(BUILDDIR)/epub3.\"\n\n.PHONY: latex\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\n.PHONY: latexpdf\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: latexpdfja\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: text\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\n.PHONY: man\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\n.PHONY: texinfo\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\n.PHONY: info\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\n.PHONY: gettext\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\n.PHONY: changes\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\n.PHONY: linkcheck\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\n.PHONY: doctest\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\n.PHONY: coverage\ncoverage:\n\t$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage\n\t@echo \"Testing of coverage in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/coverage/python.txt.\"\n\n.PHONY: xml\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\n.PHONY: pseudoxml\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n\n.PHONY: dummy\ndummy:\n\t$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy\n\t@echo\n\t@echo \"Build finished. Dummy builder generates no files.\"\n"
  },
  {
    "path": "doc/_static/config/nginx-commandment.conf",
    "content": "server {\n  listen 7443 ssl;\n  ssl_certificate /usr/local/commandment/server.crt;\n  ssl_certificate_key /usr/local/commandment/server.key;\n  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n\n  root /usr/local/commandment/commandment/static;\n  index index.html;\n\n  access_log /usr/local/commandment/log/commandment-access.log;\n  error_log /usr/local/commandment/log/commandment-error.log;\n\n  location /api {\n    include uwsgi_params;\n    uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n    uwsgi_pass unix:/usr/local/var/run/uwsgi-commandment.sock;\n  }\n\n  location /enroll {\n    include uwsgi_params;\n    uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n    uwsgi_pass unix:/usr/local/var/run/uwsgi-commandment.sock;\n  }\n\n  location /checkin {\n    include uwsgi_params;\n    uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n    uwsgi_pass unix:/usr/local/var/run/uwsgi-commandment.sock;\n  }\n\n  location /mdm {\n    include uwsgi_params;\n    uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n    uwsgi_pass unix:/usr/local/var/run/uwsgi-commandment.sock;\n  }\n\n  location /scep {\n    include uwsgi_params;\n    uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n    uwsgi_pass unix:/usr/local/var/run/uwsgi-commandment.sock;\n  }\n\n  location / {\n    try_files $uri /index.html;\n  }\n\n  location /static {\n    alias /usr/local/commandment/commandment/static;\n  }\n}\n"
  },
  {
    "path": "doc/_static/config/uwsgi-commandment.ini",
    "content": "[uwsgi]\nbase = /usr/local/commandment\n\npythonpath = %(base)\nmodule = commandment:create_app()\n\nhome = /usr/local/commandment/virtualenv\n# This might be different if you used pipenv to install the dependencies eg.\n# home = /Users/<username>/.local/share/virtualenvs/commandment-<random>\nplugins = python3\n\nenv = COMMANDMENT_SETTINGS=/usr/local/commandment/settings.cfg\n\n# This is necessary to make multi-threading / multi-processing not fail on High Sierra with\n# `+[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called.`\nenv = OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES\nmaster = true\nprocesses = 4\nenable-threads = true\n\nsocket = /usr/local/var/run/uwsgi-commandment.sock\nchmod-socket = 660\n\ndie-on-term = true\n\nlogto = /usr/local/commandment/log/uwsgi-commandment.log\n"
  },
  {
    "path": "doc/_static/uml/checkin.puml",
    "content": "@startuml\nactor Device\nboundary MDM\nentity DeviceModel\n\nDevice -> MDM: Authenticate message\nMDM -> EnrollPolicy: Check whitelist\nEnrollPolicy -> MDM: Device passed\nMDM -> DeviceModel: Update Attributes\nMDM -> DeviceModel: Clear Token\nMDM -> Device: 200 \"OK\"\n\nDevice -> MDM: TokenUpdate message\n\n\n@enduml"
  },
  {
    "path": "doc/_static/uml/commandqueue.puml",
    "content": "@startuml\nstart\n:device has commands;\n:at least one command is \"Queued\" status;\n:command does not have \"after\" date;\n\nend\n@enduml"
  },
  {
    "path": "doc/_static/uml/models/Certificate.plantuml",
    "content": "@startuml\n\nskinparam defaultFontName Courier\n\nClass certificates {\n    INTEGER            ★ id                         \n    INTEGER            ☆ rsa_private_key_id         \n    VARCHAR[20]        ⚪ discriminator              \n    VARCHAR[64]        ⚪ fingerprint                \n    DATETIME           ⚪ not_after                  \n    DATETIME           ⚪ not_before                 \n    TEXT               ⚪ pem_data                   \n    VARCHAR            ⚪ push_topic                 \n    VARCHAR[2]         ⚪ x509_c                     \n    VARCHAR[64]        ⚪ x509_cn                    \n    VARCHAR[64]        ⚪ x509_o                     \n    VARCHAR[32]        ⚪ x509_ou                    \n    VARCHAR[128]       ⚪ x509_st                    \n    INDEX[fingerprint] » ix_certificates_fingerprint\n}\n\nright footer generated by sadisplay v0.4.8\n\n@enduml"
  },
  {
    "path": "doc/_static/uml/models/Command.plantuml",
    "content": "@startuml\n\nskinparam defaultFontName Courier\n\nClass commands {\n    INTEGER       ★ id                \n    INTEGER       ☆ device_id         \n    DATETIME      ⚪ acknowledged_at   \n    DATETIME      ⚪ after             \n    TEXT          ⚪ parameters        \n    DATETIME      ⚪ queued_at         \n    VARCHAR       ⚪ request_type      \n    DATETIME      ⚪ sent_at           \n    VARCHAR[1]    ⚪ status            \n    INTEGER       ⚪ ttl               \n    CHAR[32]      ⚪ uuid              \n    INDEX[status] » ix_commands_status\n    INDEX[uuid]   » ix_commands_uuid  \n}\n\nright footer generated by sadisplay v0.4.8\n\n@enduml"
  },
  {
    "path": "doc/_static/uml/models/InstalledApplication.plantuml",
    "content": "@startuml\n\nskinparam defaultFontName Courier\n\nClass installed_applications {\n    INTEGER                  ★ id                                         \n    INTEGER                  ☆ device_id                                  \n    VARCHAR                  ⚪ bundle_identifier                          \n    BIGINT                   ⚪ bundle_size                                \n    CHAR[32]                 ⚪ device_udid                                \n    BIGINT                   ⚪ dynamic_size                               \n    BOOLEAN                  ⚪ is_validated                               \n    VARCHAR                  ⚪ name                                       \n    VARCHAR                  ⚪ short_version                              \n    VARCHAR                  ⚪ version                                    \n    INDEX[bundle_identifier] » ix_installed_applications_bundle_identifier\n    INDEX[device_udid]       » ix_installed_applications_device_udid      \n    INDEX[version]           » ix_installed_applications_version          \n}\n\nright footer generated by sadisplay v0.4.8\n\n@enduml"
  },
  {
    "path": "doc/_static/uml/models/InstalledCertificate.plantuml",
    "content": "@startuml\n\nskinparam defaultFontName Courier\n\nClass installed_certificates {\n    INTEGER                   ★ id                                          \n    INTEGER                   ☆ device_id                                   \n    BLOB                      ⚪ der_data                                    \n    CHAR[32]                  ⚪ device_udid                                 \n    VARCHAR[64]               ⚪ fingerprint_sha256                          \n    BOOLEAN                   ⚪ is_identity                                 \n    VARCHAR                   ⚪ x509_cn                                     \n    INDEX[device_udid]        » ix_installed_certificates_device_udid       \n    INDEX[fingerprint_sha256] » ix_installed_certificates_fingerprint_sha256\n}\n\nright footer generated by sadisplay v0.4.8\n\n@enduml"
  },
  {
    "path": "doc/_static/uml/models/InstalledProfile.plantuml",
    "content": "@startuml\n\nskinparam defaultFontName Courier\n\nClass installed_profiles {\n    INTEGER             ★ id                                \n    INTEGER             ☆ device_id                         \n    CHAR[32]            ⚪ device_udid                       \n    BOOLEAN             ⚪ has_removal_password              \n    BOOLEAN             ⚪ is_encrypted                      \n    VARCHAR             ⚪ payload_description               \n    VARCHAR             ⚪ payload_display_name              \n    VARCHAR             ⚪ payload_identifier                \n    VARCHAR             ⚪ payload_organization              \n    BOOLEAN             ⚪ payload_removal_disallowed        \n    CHAR[32]            ⚪ payload_uuid                      \n    INDEX[device_udid]  » ix_installed_profiles_device_udid \n    INDEX[payload_uuid] » ix_installed_profiles_payload_uuid\n}\n\nright footer generated by sadisplay v0.4.8\n\n@enduml"
  },
  {
    "path": "doc/about-mdm.rst",
    "content": "About MDM\n=========\n\nThis section is intended to give you some basic knowledge around how MDM works, so that you understand why some of\nthe prerequisites exist.\n\nSetting up an MDM requires a few different certificates:\n\n- To prove to Apple that you're allowed to use their Push Service (APNS).\n- To prove that your MDM isn't being impersonated (TLS).\n- To prove that someone isn't impersonating an Apple Device connecting to your MDM (SCEP/Identity Certificate).\n\n.. note:: I'm glossing over a lot of detail to give you a general sense of the requirements.\n\nBy far, the best in-depth explanation is the MicroMDM Blog post by Jesse Peterson on\n`Understanding MDM Certificates <https://micromdm.io/blog/certificates/>`_.\n\nAPNS MDM Certificate\n--------------------\n\nApple devices listen for Push Notifications, sent via Apple's Push Notification Service [#f1]_.\nThe notifications you send from your MDM are used to poke the devices, which contact your MDM in turn.\n\n.. uml::\n    :align: center\n\n    \"Joe's iPad\" <-> \"Apple Push Notification Service\": Listening to MDM channel\n    MDM -> \"Apple Push Notification Service\": Push to \"Joe's iPad\"\n    \"Apple Push Notification Service\" -> \"Joe's iPad\": Hey, contact the MDM!\n    \"Joe's iPad\" -> MDM: Give me the next command!\n\nTo send MDM push notifications, you will need a special Push Certificate issued by Apple.\n\nThere are several ways to get one:\n\n- Apply for an `Apple Enterprise Developer <https://developer.apple.com/programs/enterprise/>`_ account (US$300/year),\n  enabling the MDM Vendor option. You can then use this account to sign push certificate requests. The MDM Vendor option\n  is now available as a checkbox when you apply for the account.\n- Have an MDM vendor, or someone with that account sign the CSR for you. `mdmcert.download <https://mdmcert.download>`_\n  is one such service.\n- Extract the *com.apple.mgmt.* certificate from a previously installed copy of **Server.app**\n\nTLS/Web Certificate\n-------------------\n\nThe MDM protocol requires a secure encrypted connection between your devices and your MDM.\n\nThe TLS certificate on your MDM is just like any other web server, so all the same methods apply for getting one of\nthese certificates.\n\nIt's recommended to purchase an SSL certificate that will already be trusted by your devices. You can also use an\nEnterprise CA, as long as you understand that there's an extra step to allow your devices to trust the CA.\n\n.. warning:: If you are using a self-signed SSL certificate, or your Enterprise CA won't automatically be trusted by\n    your devices, then you need to make sure your devices trust the certificate. This is normally done by pushing a\n    trust profile or including trust information in the enrollment profile.\n\n    commandment has an option to bundle these certificates with the enrollment profile.\n\n\nDevice Identity Certificate\n---------------------------\n\nThe MDM protocol requires that each device enrolled with the MDM has its own certificate.\n\nThere are two options for providing the identity certificate:\n\n- Include the Identity certificate in the enrollment payloads.\n- Contact a SCEP service to issue a certificate when the device enrolls.\n\nThe second option is always the preferred method, since it allows you to use whatever existing infrastructure you have\nfor issuing certificates.\n\nIf you are testing commandment you can use `SCEPy <https://github.com/cmdmnt/SCEPy>`_ as your SCEP server.\nThis is provided as part of commandment for testing out of the box, but I would strongly encourage you to use a\ncommercial solution for SCEP.\n\n\n.. rubric:: Footnotes\n\n.. [#f1] `Push Notification Developer Guide <https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1>`_."
  },
  {
    "path": "doc/api/certificates.rst",
    "content": "Certificates\n============\n\nJSON-API Endpoints\n------------------\n\n.. http:get:: /api/v1/certificates/\n\n    Retrieve a list of all metadata about certificates stored in the system.\n\n    :query page[size]: The number of items to return for each page\n    :query page[number]: The page to fetch\n    :query filter[]: An array of filtering rules\n    :query sort: A list of fields (separated by comma) to sort ascending.\n                 Mark with a **-** minus to denote descending sort.\n    :reqheader Accept: application/vnd.api+json\n    :resheader Content-Type: application/vnd.api+json\n\n.. http:post:: /api/v1/certificates/\n\n    Add a new certificate to the system.\n\n.. http:get:: /api/v1/certificates/(int:certificate_id)\n\n    Retrieve metadata about the certificate (`certificate_id`)\n\n    **Example request**:\n\n    .. sourcecode:: http\n\n        GET /api/v1/certificates/1 HTTP/1.1\n        Accept: application/vnd.api+json\n\n    **Example response**\n\n    .. sourcecode:: http\n\n        HTTP/1.1 200 OK\n        Content-Type: application/vnd.api+json\n\n        {\n            \"data\": [{\n                \"type\": \"certificates\",\n                \"attributes\": {\n                    \"not_after\": \"2018-03-26T23:42:09+00:00\",\n                    \"pem_certificate\": \"-----BEGIN CERTIFICATE----ABCDEF==\\n-----END CERTIFICATE-----\\n\",\n                    \"subject\": \"commandment.dev\",\n                    \"purpose\": \"mdm.cacert\",\n                    \"not_before\": \"2017-03-26T23:42:09+00:00\"\n                },\n                \"id\": 1,\n                \"links\": {\n                    \"self\": \"/api/v1/certificates/1\"\n                }\n            }],\n            \"meta\": {\"count\": 1},\n            \"jsonapi\": {\"version\": \"1.0\"}\n        }\n\n    :reqheader Accept: application/vnd.api+json\n    :resheader Content-Type: application/vnd.api+json\n    :statuscode 200:\n    :statuscode 404:\n\n\n\nOther Endpoints\n---------------\n\n.. autoflask:: commandment:create_app()\n    :blueprints: api_app"
  },
  {
    "path": "doc/api/commands.rst",
    "content": "Commands\n========\n\nSummary\n-------\n\n\n\nDetail\n------\n\n.. autoflask:: commandment:create_app()\n    :blueprints: api_app\n\n.. http:get:: /api/v1/commands\n\n   Get all commands\n\n   :reqheader Accept: application/vnd.api+json\n   :resheader Content-Type: application/vnd.api+json\n\n.. http:post:: /api/v1/commands\n\n   Create a command\n\n.. http:patch:: /api/v1/commands/(int:command_id)\n\n   Update a command\n\n.. http:delete:: /api/v1/commands/(int:command_id)\n\n   Delete a command\n\n.. http:get:: /api/v1/devices/(int:device_id)/commands\n\n   Get MDM commands associated with the device specified by **device_id**\n\n.. http:all:: /api/v1/devices/(int:device_id)/relationships/commands\n\n   Attach/Detach command relationships to specific devices\n\n"
  },
  {
    "path": "doc/api/dep.rst",
    "content": "Device Enrollment Programmes\n============================\n\nSummary\n-------\n\n.. autoflask:: commandment:create_app()\n\t:blueprints: dep_app\n\n\n"
  },
  {
    "path": "doc/api/devices.rst",
    "content": "Devices\n=======\n\n.. autoflask:: commandment:create_app()\n    :blueprints: api_app\n\n.. http:get:: /api/v1/devices\n\n   Get a list of devices\n\n   :reqheader Accept: application/vnd.api+json\n   :resheader Content-Type: application/vnd.api+json\n\n.. http:get:: /api/v1/devices/(int:device_id)\n\n   Get information about a specific device.\n\n.. http:post:: /api/v1/devices\n\n   Create a new enrolled device\n\n.. http:patch:: /api/v1/devices/(int:device_id)\n\n   Update an enrolled device\n\n.. http:delete:: /api/v1/devices/(int:device_id)\n\n   Delete an enrolled device\n\n.. http:get:: /api/v1/devices/(int:device_id)/commands\n\n   Get MDM commands associated with this device.\n\n.. http:get:: /api/v1/devices/(int:device_id)/tags\n\n   Get tags associated with this device.\n\n"
  },
  {
    "path": "doc/api/index.rst",
    "content": "API Reference\n=============\n\nAlmost all responses and requests are expected to follow the `JSON-API <http://jsonapi.org>`_ standard,\nexcept in cases where binary or encoded data needs to be uploaded or downloaded,\n*OR* the endpoint is a one-off RPC style action eg. \"Erase Device\".\n\nAll of the API is generated via the `flask-rest-jsonapi <http://flask-rest-jsonapi.readthedocs.io/en/latest/>`_ library.\n\n\n.. toctree::\n    :maxdepth: 2\n\n    certificates\n    commands\n    dep\n    devices\n    organization\n"
  },
  {
    "path": "doc/api/organization.rst",
    "content": "Organization\n============\n\n:SQLAlchemy: :ref:`model-organization`\n\n.. autoflask:: commandment:create_app()\n    :blueprints: configuration_app\n"
  },
  {
    "path": "doc/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# commandment documentation build configuration file, created by\n# sphinx-quickstart on Sat Mar 25 14:50:50 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\nsys.path.insert(0, os.path.abspath('../'))\nsys.path.append(os.path.abspath('../venv/lib/python3.6/site-packages/'))\n#sys.path.append(os.path.abspath('../'))\nimport guzzle_sphinx_theme\n\nos.environ[\"COMMANDMENT_SETTINGS\"] = \"../settings.cfg\"  # Necessary to prevent autohttp.flask from raising exception\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.napoleon',\n    'sphinx.ext.todo',\n    'sphinx.ext.viewcode',\n    'sphinx.ext.githubpages',\n    'sphinxcontrib.autohttp.flask',\n    'sphinxcontrib.autohttp.flaskqref',\n    'sphinxcontrib.plantuml',\n    'guzzle_sphinx_theme'\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The encoding of source files.\n#\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'commandment'\ncopyright = u'2017, Jesse Peterson, Mosen'\nauthor = u'Jesse Peterson, Mosen'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'1.0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'1.0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = None\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n#\n# today = ''\n#\n# Else, today_fmt is used as the format for a strftime call.\n#\n# today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\n#\n# default_role = None\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n#\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n#\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n#\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# A list of ignored prefixes for module index sorting.\n# modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n# keep_warnings = False\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = True\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\n\n# html_theme = 'sphinx_rtd_theme'\nhtml_theme = 'guzzle_sphinx_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\nhtml_theme_options = {\n    \"project_nav_name\": \"Commandment\"\n}\n\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = []\n# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]\nhtml_theme_path = guzzle_sphinx_theme.html_theme_path()\n\n# The name for this set of Sphinx documents.\n# \"<project> v<release> documentation\" by default.\n#\n# html_title = u'commandment v1.0'\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n#\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n#\n# html_logo = None\n\n# The name of an image file (relative to this directory) to use as a favicon of\n# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n#\n# html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n#\n# html_extra_path = []\n\n# If not None, a 'Last updated on:' timestamp is inserted at every page\n# bottom, using the given strftime format.\n# The empty string is equivalent to '%b %d, %Y'.\n#\n# html_last_updated_fmt = None\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n#\n# html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\n#\n# html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n#\n# html_additional_pages = {}\n\n# If false, no module index is generated.\n#\n# html_domain_indices = True\n\n# If false, no index is generated.\n#\n# html_use_index = True\n\n# If true, the index is split into individual pages for each letter.\n#\n# html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\n#\n# html_show_sourcelink = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n#\n# html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n#\n# html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n#\n# html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n# html_file_suffix = None\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'\n#\n# html_search_language = 'en'\n\n# A dictionary with options for the search language support, empty by default.\n# 'ja' uses this config value.\n# 'zh' user can custom change `jieba` dictionary path.\n#\n# html_search_options = {'type': 'default'}\n\n# The name of a javascript file (relative to the configuration directory) that\n# implements a search results scorer. If empty, the default will be used.\n#\n# html_search_scorer = 'scorer.js'\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'commandmentdoc'\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n     # The paper size ('letterpaper' or 'a4paper').\n     #\n     # 'papersize': 'letterpaper',\n\n     # The font size ('10pt', '11pt' or '12pt').\n     #\n     # 'pointsize': '10pt',\n\n     # Additional stuff for the LaTeX preamble.\n     #\n     # 'preamble': '',\n\n     # Latex figure (float) alignment\n     #\n     # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'commandment.tex', u'commandment Documentation',\n     u'Jesse Peterson', 'manual'),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n#\n# latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n#\n# latex_use_parts = False\n\n# If true, show page references after internal links.\n#\n# latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n#\n# latex_show_urls = False\n\n# Documents to append as an appendix to all manuals.\n#\n# latex_appendices = []\n\n# If false, no module index is generated.\n#\n# latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'commandment', u'commandment Documentation',\n     [author], 1)\n]\n\n# If true, show URL addresses after external links.\n#\n# man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'commandment', u'commandment Documentation',\n     author, 'commandment', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n# Documents to append as an appendix to all manuals.\n#\n# texinfo_appendices = []\n\n# If false, no module index is generated.\n#\n# texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n#\n# texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n#\n# texinfo_no_detailmenu = False\n\n# Default for homebrew\nplantuml = '/usr/local/bin/plantuml'\n"
  },
  {
    "path": "doc/dev/MUSINGS.rst",
    "content": "# Musings #\n\nDynamic device groups by attribute.\nProblem: too slow to resolve group membership\n\nPossible solutions: update group membership on change?\n\n---\n\nProblem: storage of dynamic group predicates\n\n\n\n\n---\n\nGroup predicate attributes\n\nmodel\nos_version\nenrolled / not enrolled\ncheck in date/delta\ndevice capacity <>\n\nhow about IN or NOT IN\n\nhas installed application(s) =>\nhas installed profile(s) => identifier in\n\n\n\nProfile Install via Tag\n=======================\n\n- Device and profile share tag: Profile should be installed.\n- Queue profile when tag changes or when device checks in?\n    - If tag is subsequently removed, we have to manage the queue too.\n    - VS: generate install while device checks in\n- What if multiple tags are assigned to the same profile and the device is too?\n    - Checking the queue gets more complex.\n"
  },
  {
    "path": "doc/developer/guide/architecture.rst",
    "content": "Architecture\n============\n\nBackend\n-------\n\nThe backend is a `Flask <http://flask.pocoo.org/>`_ application which is expected to run on **Python 3.6+**\nusing type annotations.\n\nThe persistence layer is handled using SQLAlchemy via `Flask-SQLAlchemy <http://flask-sqlalchemy.pocoo.org/>`_.\nDatabase schema migrations are performed by `Alembic <http://alembic.zzzcomputing.com/en/latest/>`_.\n\nThe REST API follows the `JSON-API standard <http://jsonapi.org/format/>`_ using\n`Flask-REST-JSONAPI <https://flask-rest-jsonapi.readthedocs.io/en/latest/>`_.\n\nAPI that fits an RPC model better than a REST model is serialized using `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_\nwhich is what **Flask-REST-JSONAPI** uses anyway.\n\nFrontend\n--------\n\nThe frontend framework is `React <https://facebook.github.io/react/>`_, using `Redux <http://redux.js.org/>`_ for state\nmanagement. The source is written in `TypeScript <https://www.typescriptlang.org/>`_ and transpiled to ES5.\n\nThe UI framework/CSS framework is `semantic-ui <https://semantic-ui.com/>`_. We use the React components for this as well.\n\n\nServices\n--------\n\nPython is notoriously bad for multi-threaded or concurrent i/o, so it would make sense to split responsibilities across\nmicroservices. The difficulty in installation can be resolved via the use of docker-compose as the primary \"kick the tyres\"\nmethod of deployment.\n\nServices can be broken down like this:\n\n- **DEPuty**: The DEPuty should be responsible for scanning and syncing DEP devices and automatically assigning default\n  profiles to those devices.\n\n- **Frontdesk**: The frontdesk should take connections from MDM devices and relay queued commands back to those devices.\n  It can report command errors back to the main application.\n\n- **Classifier**: This should arrange devices into groups based on inventory and attributes of those devices. It can be\n  notified of changes in inventory but should be a delayed evaluation. The groups it produces should just be marked as\n  non editable by the user.\n"
  },
  {
    "path": "doc/developer/guide/building.rst",
    "content": "Building\n========\n\nBackend\n-------\n\nNone of the Python backend is compiled. So there is no build step.\n\nSince we are using type annotations, you may perform \"linting\" of sorts with `mypy <http://mypy-lang.org/>`_.\n\nFrontend\n--------\n\nFrom the **ui** directory inside the repository, there are several **npm run** scripts/commands that you can use in\neach stage of development.\n\nIf you are working on live changes and want to see the results immediately, you can run::\n\n    $ npm start\n\nThis starts a `webpack-dev-server <https://github.com/webpack/webpack-dev-server>`_, listening on ``localhost:4000`` by default.\n\nWhen flask is run with the setting ``DEBUG=True``, the javascript code is loaded from this webpack dev server on localhost.\n\nDocumentation\n-------------\n\nThe documentation is built using `Sphinx <http://www.sphinx-doc.org/>`_.\n\nSphinx, it's extensions, and the documentation theme are included in the **pipenv** developer dependencies.\n\nIf you have installed the dependencies using **pipenv** you may run::\n\n\t$ pipenv run make html\n\nfrom the :file:`doc/` directory in order to build the documentation.\n"
  },
  {
    "path": "doc/developer/guide/index.rst",
    "content": "Developer Guide\n===============\n\n\nThis guide explains how to get commandment set up for development.\n\n\n.. warning:: The guide only covers macOS at this point in time.\n\n.. toctree::\n    :maxdepth: 2\n\n    dependencies\n    building\n    architecture\n    running\n\n\n\n"
  },
  {
    "path": "doc/developer/guide/running.rst",
    "content": "Running\n=======\n\nBackend\n-------\n\nBefore you get started, make a copy of ``settings.cfg.example`` and name it ``settings.cfg``.\n\nNote the settings for ``SSL_CERTIFICATE`` and ``SSL_RSA_KEY``. SSL Certificates are required to run an MDM.\nYou'll have to generate those yourself, and they can be self-signed as long as you install trust profiles on your test\ndevice(s).\n\n\n\nIf you want to use Python debugging, it's a lot easier with the Flask development server. This is the recommended way\nto develop commandment.\n\nBecause MDM requires an SSL connection, the ``flask run`` command won't work out of the box.\n\nFor this, We've provided a command line application which runs the development server with an SSL context.\nThe command line application is contained in ``commandment.cli``.\n\nAssuming you have installed all the Pipenv dependencies, run::\n\n\tCOMMANDMENT_SETTINGS=/path/to/settings.cfg pipenv run commandment\n\nFrom the checked-out git repository.\n\nThis will start an SSL server on port 5443, using the private key and certificate specified in the :file:`settings.cfg`.\n\n.. note:: The backend will assume that you are also running a webpack-dev-server [#f1]_ (front end dev server) if the\n\tsetting ``DEBUG = True``. This is extremely useful for seeing javascript changes on the fly.\n\nDatabase\n--------\n\ncommandment is configured by default to use an SQLite database (commandment.db) in the same directory as the repository.\n\nTo initialise the database you should use the ``alembic`` tool, which was part of our python dependencies.\n\nTo do this, change to the commandment directory and run::\n\n\t$ pipenv run alembic upgrade head\n\nThis runs the alembic tool inside the pipenv virtual environment.\n\nFrontend\n--------\n\nWhen running the backend on the dev server, front-end assets will be loaded from **localhost** on port **4000**.\n\nTo start the webpack dev server [#f1]_, run the following command inside the :file:`ui` directory::\n\n\tNODE_ENV=development npm start\n\nYou should see some output indicating that the webpack-dev-server is running on port 4000.\n\nThe webpack dev server is configured to use the same SSL certificate and private key as the Flask backend by default.\nIn some browsers you will have to trust BOTH the python backend on 5443 and the webpack-dev-server on port 4000.\n\nIt helps if they are using the same hostname and ssl certificates.\n\n\n\n.. rubric:: Footnotes\n\n.. [#f1] `webpack-dev-server <https://webpack.js.org/configuration/dev-server/>`_."
  },
  {
    "path": "doc/developer/index.rst",
    "content": "#######################\nDeveloper Documentation\n#######################\n\n.. toctree::\n    :maxdepth: 2\n\n    install\n    guide/index\n\n\n    "
  },
  {
    "path": "doc/developer/microservices.rst",
    "content": "Microservices Architecture\n==========================\n\nMDM only has certain limitations which means that microservices have only a limited range of definition in terms of where\ndependent services can live.\n\nHere's some ideas for services\n\n\nDEP Scanner + Default Profiler\n------------------------------\n\n- Some process needs to scan/sync DEP\n- This is a good point in time to evaluate which DEP profile should be assigned to the devices as they come in.\n- If there was a rules based evaluation of DEP profile assignment, that could also happen here.\n- Manual DEP assignment does NOT have to live here, because it is performed imperatively against collections of objects.\n- This process can create new device records when it finds new DEP records. These can exist in a \"pre-enrolled\" state.\n\n\nAPNS Pusher\n-----------\n\n- Most MDM systems have some sort of Queue monitor/APNS push watcher.\n- After a certain amount of time, devices with >0 commands to send are evaluated.\n- Some commands are imperative and you would expect them to happen almost immediately (Shutdown, Restart). with exception\n  to device collections larger than 100, where the push may take some time.\n- Some commands are expected to happen in good time (InstallProfile, InstallApplication).\n\n\nInventory\n---------\n\n- End users expect REASONABLY recent device inventory.\n- Some process needs to Queue inventory commands at a refresh interval, but not queue all devices at once.\n- It must also not queue commands if they are already queued.\n- It must also not queue commands for recently refreshed inventory.\n\n\nProfiles\n--------\n\n- Try not to introspect profile payload structure because it can literally be anything almost.\n- Examine desired profiles vs installed profiles and create a command for it.\n\n\nApplications\n------------\n\n- Same theoretical application as PRofiles but with a different object type.\n\nCalculated Groups (Classifier)\n------------------------------\n\n- Isolate a sub-population of devices by attribute predicates.\n- Many cloud providers de-prioritise the calculation of these groups in order to reduce impact, but this also results in\n  sluggish feedback.\n- Tactics for speeding up or lessening impact of calculated groups:\n  - Do not recalculate if inventory data did not change: therefore track devices which did change in the last x duration.\n  - Newly created groups must force a recalculation of membership immediately to provide feedback to the user.\n  - Compound predicates are the union intersection of simple predicates, so maybe this can be exploited to lower the cost\n    of group calculation.\n  - Consider groupable attributes for indexing\n- Groups can be used to functionally identify the workflow state of a device from its pre-enrolled DEP state through\n  DeviceConfigured into enrolled.\n- Pre-defined groups:\n  - By form factor (Desktop, Tablet, Phone, ATV)\n  - Workflow state (DEP -> AwaitConfiguration -> Enrolled -> Stale -> Unenrolled)\n  - OS Flavour+Major Version (becomes a derivative of form factor groups)\n\t- Minor Version (becomes a derivative of major version)\n  - Cellular v non cellular (subset of union Tablet+Phone)\n- Freestyle composite groups:\n  - Nominate a pre-defined group to limit calculation results.\n  - Enforce a predicate on that.\n\n\nReaper\n------\n\n- Scan age of devices and mark them as Stale if no communications recently.\n- Unenroll devices once they have not communicated in a long amount of time,\n"
  },
  {
    "path": "doc/guides/INSTALL.md",
    "content": "## Requirements\n\n##### Software Requirements\n\n* [Python](https://www.python.org/) 2.7+\n* [cryptography](https://cryptography.io/en/latest/)\n* [Flask](http://flask.pocoo.org/)\n* [SQLAlchemy](http://www.sqlalchemy.org/)\n* [SQLite](https://www.sqlite.org/) (default database)\n\n##### Apple MDM push notification certificate\n\nYou will need an Apple MDM APNs certificate in order to send MDM push notifications to devices. To get one, for now, you'll need to have an [Apple Enterprise Developer](https://developer.apple.com/programs/enterprise/) account (US$300/year).\n\nEventually this software may support an ability to assist in getting one of these Push Certificates from Apple's servers. But for now please read [Pepijn Bruienne's excellent blog post](http://enterprisemac.bruienne.com/2015/06/06/mdm-azing-setting-up-your-own-mdm-server/) on how to get this certificate. Note that that post includes information on setting up [Project iMAS MDM server](https://github.com/project-imas/mdm-server) and so some of his post isn't directly relevant to getting the Apple push certificate.\n\nNote we don't yet deal with any intermediate steps or certificates with this MDM software (such as Vendor MDM Certificates or generating and submitting CSRs to Apple, etc.). Those steps are required, but they have little to do with running this software. We just require the very end product of the actual MDM push certificate (certificate subject that contains `com.apple.mgmt.*` and associated private key). It needs to be an unencrypted certificate and private key in PEM form for later import into the MDM server. This may require an additional export and unencryption of the exported certificate.\n\n##### DNS and network configuration for SSL hostname matching\n\nIt is possible to use self-signed certificates with an Apple MDM system. However the hostname and SSL certificate subject matching is strict and an enrolling device needs to trust the MDM server's HTTPS certificate.\n\nThe trust is established when the device enrolls: by default the HTTPS certificate is included as a profile payload for the device to trust when enrolling. However the hostname matching still needs to be successful. Practically speaking this means that if you intend for your devices to access your MDM via something like https://mymdm.example.com:5443/ then the MDM's web server certificate must also \"match\" this name (in typical SSL matching rules which includes wildcards and such) by having a certificate subject Common Name (CN) of `mymdm.example.com`.\n\nIf you have a DNS record already setup and don't want to use the default `mymdm.example.com` name then you'll need to generate a new web server certificate. Instructions for doing so are below, **but remember to restart the development webserver** after you've *generated a new* web certificate with an appropriate Common Name (CN) and *deleted the old* web certificate.\n\nWhile not recommended it's possible to use a `/etc/hosts` entry to test the system including enrollment and MDM operation of a single system on the same host as the server. Useful for quick and dirty virtual machine testing.\n\n_**Note:** while IP address certificates appear to work for MDM on iOS not much luck was had with OS X doing this. Besides this it is [not recommended](http://tools.ietf.org/html/rfc6125#section-1.7.2) to use IP addresses in the Common Name field for certificates. Also the CA/B has [deprecated](https://cabforum.org/internal-names/) *internal* IP addresses in certificate subjects from public Certificate Authorities. In other words: best not to go this route._\n\n## Installing the requirements\n\n#### Setting up on OS X for development and testing in a Python virtual environment\n\nInstructions for OS X 10.10. These aren't definitive instructions for getting the dependencies installed on OS X. See also [Greg Neagle's post](https://groups.google.com/d/msg/ossmdm/onF7KFWnIa4/LMMRu7OrBiIJ) on getting Project IMAS requirements setup (which are similar to our requirements).\n\n\n##### Install & create virtualenv\n\n[virtualenv](https://virtualenv.pypa.io/en/latest/) is a tool to create isolated Python environments. We want to use this so we're not installing Python packages to the system Python locations and to have a self-contained Python environment. We do have to install virtualenv to the system locations, however:\n\n```bash\nsudo easy_install virtualenv\n```\n\nThen, create the virtualenv and import the configuration into the current shell:\n\n```bash\nvirtualenv commandment-venv\nsource commandment-venv/bin/activate\n```\n\nAfter `source`ing the virtualenv your bash prompt should have changed to include the `commandment-venv`. From:\n\n\n```bash\nMac:Desktop jesse$\n```\n\nTo:\n\n```bash\n(commandment-venv)Mac:Desktop jesse$\n```\n\n##### Clone the source\n\nUse Git to clone the GitHub source of commandment:\n\n```bash\ngit clone git@github.com:jessepeterson/commandment.git\ncd commandment\n```\n\n##### Install Python project dependencies \n\nWhile still in the `commandment-venv`-activated virtualenv, and in the `commandment` checked-out source code directory, tell `pip` to install the requirements:\n\n```bash\npip install -r requirements.txt\n```\n\nAssuming all of the above steps completed without problems you should be good to go now. Next steps are to run the server and begin in-webapp configuration.\n\nFor OS X 10.9 users: M2Crypto can't find the OpenSSL header files on the system. To get around this download the latest tarball of [M2Crypto from PyPi](https://pypi.python.org/pypi/M2Crypto), unpack it, change to it's directory, and while still in the virtualenv run:\n\n```bash\npython setup.py install build_ext --openssl=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr\n```\n\nThen re-run the `pip -r` command from above to get the rest of the dependencies which should install fine.\n\n## Server installation and setup\n\n### 1. Start runserver.py and visit web site\n\n```bash\n./runserver.py\n```\n\nThis will immediately configure application settings, setup the database's ORM (SQLAlchemy), create the database schema (in the default SQLite database `mdm.db` in the current directory), create initial self-signed SSL certificates and keys, configure the development web server and start the application. Soon after start it should start listening on the default port 5443 and amongst the verbose output should be these lines:\n\n```\n * Running on https://0.0.0.0:5443/ (Press CTRL+C to quit)\n * Restarting with stat\n```\n\nThis means the server has started and is listening for connections. Go visit https://127.0.0.1:5443/ (remembering the http**S**:// as it is a secure site). You'll get an SSL certificate warning prompt as the server is using a newly generated self-signed certificate. But you can ignore that for now and continue to the site.\n\nYou should be presented with the enrollment interface. Don't try to enroll devices yet or click on the enroll link. We're not setup yet.\n\n### 2. Add your push certificate to the system\n\nPer the above requirements you'll need your Apple MDM push certificate and private key in unencrypted PEM form. Once you have those then visit https://127.0.0.1:5443/admin/certificates. You should be presented with a list of all the non-device identity certificates currently configured in the system. One of items listed will be \"APNS MDM Push Certificate (Required)\" and a \"(Certificate Missing)\" in red where the subject would be. To the right of this table row click the \"[ Add ]\" link. This will give us two large text fields to paste in the PEM certificate and private key into the system. Do so and click Submit. If all worked then you should now see that the Subject column of the APNS MDM Push Certificate row is filled in with a `UID=com.apple.mgmt.*, CN=APSP:*` entry where the asterisks are UUID-looking values. The APNS certificate is now in the system ready for use.\n\n### 3. Setup an appropriate web server SSL certificate and verify\n\nPer the above requirements you're likely not going to use the default server name of `mymdm.example.com`. So we'll want to generate ourselves a more appropriate self-signed certificate. This can be done in the app itself. Visit the admin certificates page again. You should see the default `CN=mymdm.example.com` certificate under the \"MDM Web Server Certificate\". Before you delete this certificate realize that without a proper web certificate the development server cannot start. Delete the `CN=mymdm.example.com` certificate and then click the \"[ Generate New ]\" link to make a new one. The only type of certificate (currently) allowed that you can create is a web server certificate. Fill in the various fields of the of the new certificate but **importantly** using a Common Name that matches the DNS name of the server.\n\nNow **restart the web server** (press controll-C in the server Terminal) to start using this new certificate. Navigate to your server URL using the new DNS name that you just created and the same port number. As an example if we used `newtestmdm.example.com` to generate a certificate then navigate to https://newtestmdm.example.com:5443/. You should still get a browser prompt for a certificate (it is still a self-signed certificate) but the name of the certificate should be the new name that you gave it when you generated the certificate. Verify this when the certificate prompt comes up. For our example here the certificate subject common name would be `newtestmdm.example.com`. If it does not match the URL you're using then expect problems enrolling devices and using the MDM server.\n\n### 4. Create MDM certificate configuration\n\nNow that we have correct DNS & web server certificate, we need to create the MDM configuration. In the web admin click the \"Config\" link at the top. Fill out the form as it's page describes. The Profile prefix is often just the domain name in reverse \"domain component\" form. For our example this might be \"com.example.newtestmdm\". Take special care for the hostname and web server port. This field should match the hostname used for the web server certificate above, and the same port number (default 5443). There should only be one Certificate Authority and Push Certificate available to select at this stage. ***Keep in mind** that the MDM Push certificate \"topic\" and hostname/port (MDM URLs) cannot change for the lifetime that a device is enrolled: this is a specific requirement of MDM profile payloads.* Now Click Submit. The Config Admin page should reload but now with some values statically set.\n\nYou should now be able to enroll a device!\n\n### 5. Enroll a device\n\n_**WARNING:** It goes without saying MDM systems are powerful. They can lock, wipe, or otherwise disable a device. It's recommended to test using a device that is not important in case of accidental or inadvertant data loss or lock-out (which would require a reset)._\n\nOn a device to be enrolled go to the landing page of the MDM system. This is the root URL we accessed above. In our example this is https://newtestmdm.example.com:5443/. Click the link to enroll. This will dynamically generate an MDM enrollment profile that should trust the web server certificate, includes the device's newly generated identity certificate (signed by the built-in CA), the MDM payload, and enroll the device.\n\nIf the device was successfully enrolled then you should be able to visit the devices list (https://newtestmdm.example.com:5443/admin/devices in our example setup) to view the newly enrolled device. Sometimes the very first MDM notification is not sent (and thus the device details are missing from the table) so the \"[ Send Push Notif. ]\" button can be used to request the machine specifically check-in.\n\nCongrats! If it enrolled and you see device details (name, serial number, etc.) then it's working! Now something a little more useful..\n\n_**Warning:** Apple recommends MDM developers use a SCEP system to enroll certificates with an MDM vendor. To simplify setup we do not do this and instead generate a device identity certificate directly embedded in the enrollment profile. For iOS this is likely fine as the enrollment profile isn't by default downloaded anywhere. But on OS X the enrollment profile is downloaded to disk (default browser action) which means the device's identity certificate is stored on the filesystem trivially accessible (usually just in the Downloads folder). Given access to the enrollment profile one can trivially spoof the device to the MDM system. This means you may want to enroll OS X devices using a script or other technique than just having users simply enroll to make sure the original enrollment profile is deleted after enrollment. The device identity private key is not stored in the MDM server after the enrollment profile is generated but it is embedded in the enrollment profile._\n\n### 6. Create a device group and apply a (the) example profile to it\n\nFrom the admin area of the web app go to the Groups page. Add a group naming it however you wish. Now go to the Profiles admin page. Create a new one. Uncheck the \"Allow iTunes\" checkbox (note this profile only does anything on iOS devices). And click Add Profile. Edit this newly created Profile. Select the newly created group under the Group Applicablility section. You've now associated that profile to be installed on any devices in that group.\n\n### 7. Assign the device to this group to apply the profile to it\n\nGo to the Devices list. Click the \"[ View Device ]\" link for your newly enrolled device. Select the newly assigned profile group you created and click the 'Update Device Group' link. Performing this action will create the new group membership, queue a new MDM command to install the profile on the device, and send a push notification to the device to run this command.\n\nIf all worked then the iOS device you enrolled should have it's iTunes icon removed from it's home screen. You should be able to unassign the device from the group to put it back.\n\n*Currently only device group memebership triggers profile updates, further functionality is coming.*\n"
  },
  {
    "path": "doc/guides/nginx.rst",
    "content": "Nginx Configuration\n===================\n\nIf you are using commandment behind Nginx, Nginx will be terminating the SSL connection, therefore commandment is unable\nto manage the SSL certificates for you. You should always pass the client certificate back up to commandment so that\nthe validity of the device certificate can be determined using CA's stored in commandment.\n\nWith uWSGI\n----------\n\nPrerequisites:\n\n- Python 3.5+\n    + On macOS: ``brew install python3``\n- uWSGI\n    + On macOS: ``brew install uwsgi --with-python3``\n    + Linux DEB: ``uwsgi-plugin-python3``\n- uWSGI python3 plugin\n\nExample configuration (with uWSGI)::\n\n    server {\n        listen 443 ssl;\n        server_name commandment.dev;\n        ssl_certificate commandment.dev.crt;\n        ssl_certificate_key commandment.dev.key;\n        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n        ssl_verify_client optional_no_ca;\n\n        root /path/to/commandment/static;\n        access_log commandment-access.log;\n        error_log commandment-error.log;\n\n        location / { try_files $uri @commandment; }\n        location @commandment {\n            include uwsgi_params;\n            uwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n            uwsgi_pass unix:/tmp/uwsgi.sock;\n        }\n    }\n\n\nReferences:\n- http://uwsgi-docs.readthedocs.io/en/latest/Nginx.html\n- http://flask.pocoo.org/docs/0.12/deploying/uwsgi/\n\nWith Gunicorn\n-------------\n\n- ``pip3 install gunicorn``\n\n\n    gunicorn -w 4 commandment:create_app\n\n\n\nWith Phusion Passenger\n----------------------\n\nExample configuration::\n\n    server {\n        listen 443 ssl;\n        server_name commandment.dev;\n        ssl_certificate commandment.dev.crt;\n        ssl_certificate_key commandment.dev.key;\n        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n        ssl_verify_client optional_no_ca;\n\n        root /path/to/commandment/static;\n        access_log commandment-access.log;\n        error_log commandment-error.log;\n\n        passenger_enabled on;\n    }\n\n\n"
  },
  {
    "path": "doc/guides/scep.rst",
    "content": "Verifying CMS Replies::\n\n    /usr/local/Cellar/openssl/1.0.2k/bin/openssl cms -verify -in /tmp/reply.bin -inform DER -noverify\n"
  },
  {
    "path": "doc/index.rst",
    "content": ".. commandment documentation master file, created by\n   sphinx-quickstart on Sat Mar 25 14:50:50 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to commandment's documentation!\n=======================================\n\nContents:\n\n.. toctree::\n   :maxdepth: 2\n\n   about-mdm\n   installing/macos\n   user/configuration\n   user/dep\n   developer/index\n   api/index\n   internal/index\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n\n"
  },
  {
    "path": "doc/installing/index.rst",
    "content": "==========\nInstalling\n==========\n\n"
  },
  {
    "path": "doc/installing/install.rst",
    "content": "Installation\n============\n\nDependencies\n------------\n\nThese are the dependencies for developing with commandment. You won't need all of these to host it.\n\nQuick Start (Using Homebrew)\n----------------------------\n\nFirst clone the repository, then install dependencies::\n\n\t$ brew install python3 nodejs yarn\n\t$ pip install pipenv\n\t$ pipenv install\n\t$ cd ui && yarn install\n\n\nThe long version\n----------------\n\nPython 3.6+\n^^^^^^^^^^^\n\nCommandment is written using Python 3.6, and uses type annotations as provided by the\n`typing <https://docs.python.org/3/library/typing.html>`_ module.\n\nmacOS ships with Python 2.7, so you will need to have a separate instance of Python 3.6 installed on your system.\nYou can use `Homebrew <https://brew.sh>`_ to install Python 3.6 alongside the system provided Python 2.7.\n\nIt's as easy as::\n\n    $ brew install python3\n\nYou can also get an isolated environment using something such as `Anaconda <https://www.continuum.io/downloads>`_, which\nis not covered here.\n\nOn Linux you should be able to install python 3 using your distributions packaging tools such as **yum** or **apt-get**.\n\nNodeJS 7+\n^^^^^^^^^\n\nAll of the front end tooling requires NodeJS. You can download and install an official package from `here <https://nodejs.org/en/>`_.\nI use `nvm <https://github.com/creationix/nvm>`_ to run multiple NodeJS versions at a time, for testing purposes.\nYou may also run::\n\n\t$ brew install nodejs\n\n\nSetting up the environment\n--------------------------\n\nThis part will assume that you have now cloned the git repository somewhere on your system. Usually within your own\nhome folder.\n\nPipenv\n^^^^^^\n\nTo download the Python dependencies, you first need `pipenv <https://docs.pipenv.org/>`_. You can install pipenv by\nrunning::\n\n\t$ pip install pipenv\n\nSee the **pipenv** documentation for information about how to install it on other Linux distributions.\n\nPython dependencies\n^^^^^^^^^^^^^^^^^^^\n\nTo install python dependencies, change to the commandment directory and run::\n\n\t$ pipenv install\n\nThis should download and install all python requirements into a new virtualenv.\n\n.. note:: This supersedes the :file:`requirements.txt` method.\n\nFront end dependencies\n----------------------\n\nAll of the front end code is contained within the **ui** subdirectory, so make that your current working directory.\n\nFirst, you need to install all of the **node** dependencies. For this i recommend `yarn <https://yarnpkg.com>`_, which you\ncan install by running::\n\n    $ brew install yarn\n\nThen, to install all front end dependencies you can run::\n\n    $ yarn install\n\nFrom the ui directory.\n\nYou now have the tools to develop both the backend and front end code.\n"
  },
  {
    "path": "doc/installing/macos.rst",
    "content": "Installation\n============\n\nmacOS\n-----\n\n.. note:: macOS is not a recommended platform for hosting an MDM. However, you can use it to test commandment.\n\nManual Installation\n^^^^^^^^^^^^^^^^^^^\n\n- Install `Homebrew <https://brew.sh/>`_.\n- Install Pre-requisites::\n\n    $ brew install python3\n    $ brew install uwsgi --with-python --with-python3\n    $ brew install nginx\n\n- *TODO: upload release tarball. For now you will need to git clone* Unpack commandment to :file:`/usr/local/commandment`.\n- Use this example NGiNX configuration (:download:`download </_static/config/nginx-commandment.conf>`).\n  Copy the downloaded file to :file:`/usr/local/etc/nginx/servers/commandment.conf`.\n- Use this example uWSGI configuration (:download:`download </_static/config/uwsgi-commandment.ini>`).\n  Copy the downloaded file to :file:`/usr/local/etc/uwsgi/apps-enabled/uwsgi-commandment.ini`.\n\nSSL\n^^^\n\nMDM more or less requires an SSL certificate. The example NGiNX configuration file above expects a private key, located\nat :file:`/usr/local/commandment/server.key` and a certificate, located at :file:`/usr/local/commandment/server.crt`.\n\nFor a production instance, you will require an SSL certificate issued by a 3rd party for the chosen domain. However,\nas this is a macOS installation guide, You may also use a self-signed certificate.\n\n.. note:: Creating SSL certificates is outside of the scope of this document.\n\nHandy tip for extracting PEM/key pair out of a .p12 exported by Keychain Assistant::\n\n\topenssl pkcs12 -in yourP12File.p12 -nocerts -out privatekey.pem\n\topenssl pkcs12 -in yourP12File.p12 -clcerts -nokeys -out certificate.pem\n\nFor converting DER to PEM::\n\n\topenssl x509 -inform DER -outform PEM -text -in mykey.der -out mykey.pem\n\n\nPush Notification Certificate\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nYou need a push certificate to tell devices when to check-in.\n\nYou have three options:\n\n- Sign up for an Apple Enterprise Developer Account (ca. $400 USD). Enable the MDM option and sign your own Push Certificate\n  request.\n- Register on `mdmcert.download <https://mdmcert.download/>`_.\n- Export the Push Certificate from Profile Manager (really not supported).\n\nThis guide follows the **mdmcert.download** workflow.\n\n- First, register on `mdmcert.download <https://mdmcert.download/>`_. The e-mail address you use will be the one that\n  receives all notifications and certificate signing requests.\n- *TODO* visit ``/apns/mdmcert`` using the web ui to request a new CSR.\n- *TODO* upload the CSR received in your e-mail to this same page.\n- *TODO* download the decrypted CSR for upload to the APNS portal.\n- Go to the Apple Push Certificate Portal and upload the CSR.\n- Download the resulting push certificate.\n\n\n.. note:: At this stage you should have an MDM Push Certificate and SSL Certificate ready so that your devices will talk\n    to the MDM service. You should also decide whether to use `SCEPy <https://github.com/mosen/SCEPy>`_ for testing or\n    another SCEP service such as Microsoft NDES.\n\nConfiguration\n^^^^^^^^^^^^^\n\nAn example configuration file, called :file:`settings.cfg.example` is supplied with commandment.\n\nYou should copy this file to a file named :file:`settings.cfg` and make updates as needed.\n\nEach setting is documented within the file.\n\n"
  },
  {
    "path": "doc/installing/ubuntu-server.rst",
    "content": "Installation on Ubuntu\n======================\n\nThis guide was written using Ubuntu Server 19.04. Amendments are welcome for different versions.\nThis guide assumes you are a regular user who is part of the sudoers group.\n\n1. Dependencies\n---------------\n\nInstall packages::\n\n\tsudo apt-get update\n\tsudo apt install -y python3 python3-venv nginx uwsgi uwsgi-plugin-python3 nodejs npm pipenv\n\nClone the project into /var/www::\n\n\tsudo git clone https://github.com/cmdmnt/commandment.git /var/www/commandment\n\nInstall backend dependencies::\n\n\t$ cd /var/www/commandment\n\t$ sudo python3 -m venv virtualenv\n\t$ . ./virtualenv/bin/activate\n\t(virtualenv)$ sudo -E pipenv --python /usr/bin/python3 install\n\nInstall frontend dependencies::\n\n\t$ cd /var/www/commandment/ui\n\t$ sudo npm install\n\n2. Backend\n----------\n\n2.1 uWSGI\n^^^^^^^^^\n\nuWSGI runs multiple copies of the backend to service requests.\n\nCreate a new uWSGI configuration in /etc/uwsgi/apps-available/commandment.ini\n\nIf you are following this guide use the template below, which you can adjust later if you want to move locations of\nvarious components::\n\n\tcat <<EOF |sudo tee /etc/uwsgi/apps-available/commandment.ini\n\n\t\t[uwsgi]\n\t\tbase = /var/www/commandment\n\t\tpythonpath = %(base)\n\t\tmodule = commandment:create_app()\n\n\t\thome = /var/www/commandment/virtualenv\n\t\tplugins = python3\n\n\t\tenv = COMMANDMENT_SETTINGS=/var/www/commandment/settings.cfg\n\t\tmaster = true\n\t\tprocesses = 4\n\t\tenable-threads = true\n\n\t\tsocket = /var/run/uwsgi-commandment.sock\n\t\tchmod-socket = 660\n\n\t\tdie-on-term = true\n\n\t\t# Use this log to debug startup or app failures\n\t\tlogto = /var/log/uwsgi/app/commandment.log\n\tEOF\n\n\nSymlink to **apps-enabled**::\n\n\tsudo ln -s /etc/uwsgi/apps-available/commandment.ini /etc/uwsgi/apps-enabled/commandment.ini\n\nVerify that the backend actually starts::\n\n\t$ sudo systemctl restart uwsgi\n\t$ sudo tail -f /var/log/uwsgi/app/commandment.log\n\nYou will see errors about the settings file missing, because we haven't configured commandment yet!\nYou should at least see something like::\n\n\tSun Jun  9 12:55:41 2019 - spawned uWSGI master process (pid: 13435)\n\tSun Jun  9 12:55:41 2019 - spawned uWSGI worker 1 (pid: 13442, cores: 1)\n\n\n2.2 NGiNX\n^^^^^^^^^\n\nConfigure NGiNX to pass requests to uWSGI (if backend is required), or static assets (for frontend).\n\nDecide on a DNS name for your installation. This will later require certificates, and your devices cannot be moved without\nre-enrollment. So it's going to be a pain to change. For a sandbox LAN install you might even choose a bonjour name\n\nGenerate a self-signed or properly signed SSL certificate for your fqdn.\n\nAdd an NGiNX configuration accordingly to /etc/nginx/sites-available/commandment.conf, using the following as a guide::\n\n\tcat <<\"EOF\" |sudo tee /etc/nginx/sites-available/commandment.conf\n\t\tserver {\n\t\t  listen 443 ssl;\n\t\t  ssl_certificate /etc/ssl/certs/commandment.crt;\n\t\t  ssl_certificate_key /etc/ssl/private/commandment.key;\n\t\t  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n\n\t\t  root /var/www/commandment/commandment/static;\n\t\t  index index.html;\n\n\t\t  access_log /var/log/nginx/commandment-access.log;\n\t\t  error_log /var/log/nginx/commandment-error.log;\n\n\t\t  location /api {\n\t\t\tinclude uwsgi_params;\n\t\t\tuwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n\t\t\tuwsgi_pass unix:/var/run/uwsgi-commandment.sock;\n\t\t  }\n\n\t\t  location /enroll {\n\t\t\tinclude uwsgi_params;\n\t\t\tuwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n\t\t\tuwsgi_pass unix:/var/run/uwsgi-commandment.sock;\n\t\t  }\n\n\t\t  location /checkin {\n\t\t\tinclude uwsgi_params;\n\t\t\tuwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n\t\t\tuwsgi_pass unix:/var/run/uwsgi-commandment.sock;\n\t\t  }\n\n\t\t  location /mdm {\n\t\t\tinclude uwsgi_params;\n\t\t\tuwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n\t\t\tuwsgi_pass unix:/var/run/uwsgi-commandment.sock;\n\t\t  }\n\n\t\t  location /scep {\n\t\t\tinclude uwsgi_params;\n\t\t\tuwsgi_param HTTP_X_CLIENT_CERT $ssl_client_cert;\n\t\t\tuwsgi_pass unix:/var/run/uwsgi-commandment.sock;\n\t\t  }\n\n\t\t  location / {\n\t\t\ttry_files $uri /index.html;\n\t\t  }\n\n\t\t  location /static {\n\t\t\talias /var/www/commandment/commandment/static;\n\t\t  }\n\t\t}\n\tEOF\n\nSymlink to **sites-enabled**::\n\n\tsudo ln -s /etc/nginx/sites-available/commandment.conf /etc/nginx/sites-enabled/commandment.conf\n\n3 SSL Certificate(s)\n--------------------\n\nNGiNX will fail to start until we actually create an SSL certificate for this site.\n\nIf this is a non-public, development, sandbox environment you can use a self-signed certificate.\nThis means that you're either developing with commandment or you dont mind being restricted to a home (W)LAN network.\nI usually couple this with Bonjour for a hassle free testbed, hosting on something like computer.local.\n\nIf you ever intend to make it public (internet) facing, you need to sort out an SSL certificate and DNS name that are\nexternally verifiable and visible. This means having a DNS name or hosting on a service which gives you a static domain\nname for your site AND getting a 3rd party certificate issued. The cheapest recommended option for that is to use\nLetsEncrypt.\n\n3.1 Self-Signed Certificate(s)\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTo use self-signed certificates, first check that your hostname will be the fqdn that devices can access your machine with::\n\n\t$ hostnamectl\n\nIf the **Static hostname:** can't be resolved from another computer or device, the SSL cert generated in the next section\nwon't work.\n\nGenerate self-signed certificates::\n\n\t$ sudo apt install ssl-cert\n\t$ sudo make-ssl-cert generate-default-snakeoil --force-overwrite\n\nThis will generate a cert/key pair in /etc/ssl/certs/ssl-cert-snakeoil.pem and /etc/ssl/private/ssl-cert-snakeoil.key\nrespectively. Update the ``ssl_certificate`` and ``ssl_certificate_key`` directives in the NGiNX config.\n\n\n\n"
  },
  {
    "path": "doc/internal/api/api.rst",
    "content": "Non-Standardised API\n====================\n\n.. qrefflask:: commandment:create_app()\n    :blueprints: flat_api\n    :endpoints:\n\n.. autoflask:: commandment:create_app()\n    :blueprints: flat_api\n    :endpoints:\n"
  },
  {
    "path": "doc/internal/api/index.rst",
    "content": "API\n===\n\nThe API is currently split into two categories. Most of the API adheres to the\n`JSON-API Specification <http://jsonapi.org/format/>`_. Some things such as RPC style calls or singleton objects\nwouldn't make sense in this context, so they're placed into a flat_api blueprint.\n\n.. toctree::\n    :maxdepth: 2\n\n    api.rst\n    json-api.rst\n\n"
  },
  {
    "path": "doc/internal/api/json-api.rst",
    "content": "JSON-API v1\n===========\n\n.. qrefflask:: commandment:create_app()\n    :blueprints: api_app\n    :endpoints:\n\n.. autoflask:: commandment:create_app()\n    :blueprints: api_app\n    :endpoints:\n"
  },
  {
    "path": "doc/internal/cms/decorators.rst",
    "content": "Decorators\n==========\n\n.. automodule:: commandment.cms.decorators\n    :members:\n\n"
  },
  {
    "path": "doc/internal/cms/index.rst",
    "content": "CMS - Cryptographic Message Syntax / PKCS#7\n===========================================\n\nDetails of the **commandment.cms** package.\n\nThis package implements most of the CMS / PKCS#7 functionality with the aid of *asn1crypto* and *cryptography*.\n\n.. toctree::\n    :maxdepth: 2\n\n    decorators\n\n"
  },
  {
    "path": "doc/internal/core/index.rst",
    "content": "Commandment Core\n================\n\nDetails of the **commandment** core package\n\n.. toctree::\n    :maxdepth: 2\n\n    models/index\n    signals\n\n"
  },
  {
    "path": "doc/internal/core/models/certificate.rst",
    "content": ".. _model-certificate:\n\nCertificate\n===========\n\n.. uml:: /_static/uml/models/Certificate.plantuml\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: Certificate\n    :members:\n\n\n"
  },
  {
    "path": "doc/internal/core/models/certificate_request.rst",
    "content": "CertificateRequest\n==================\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: CertificateSigningRequest\n    :members:\n    :show-inheritance:\n\n\n"
  },
  {
    "path": "doc/internal/core/models/command.rst",
    "content": ".. _model-command:\n\nCommand\n=======\n\n.. uml:: /_static/uml/models/Command.plantuml\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: Command\n    :members:\n\n.. autoclass:: CommandStatus\n    :members:\n\n"
  },
  {
    "path": "doc/internal/core/models/device.rst",
    "content": "Device\n======\n\n.. uml:: /_static/uml/models/Device.plantuml\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: Device\n    :members:\n\n"
  },
  {
    "path": "doc/internal/core/models/index.rst",
    "content": "ORM (SQLAlchemy) Models\n=======================\n\n.. toctree::\n    :maxdepth: 2\n\n    certificate.rst\n    certificate_request.rst\n    command.rst\n    device.rst\n    organization.rst\n    installed_profile\n    installed_certificate\n    installed_application\n    profile.rst\n    rsa_private_key.rst\n"
  },
  {
    "path": "doc/internal/core/models/installed_application.rst",
    "content": "InstalledApplication\n====================\n\n.. uml:: /_static/uml/models/InstalledApplication.plantuml\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: InstalledApplication\n    :members:"
  },
  {
    "path": "doc/internal/core/models/installed_certificate.rst",
    "content": "InstalledCertificate\n====================\n\n.. uml:: /_static/uml/models/InstalledCertificate.plantuml\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: InstalledCertificate\n    :members:"
  },
  {
    "path": "doc/internal/core/models/installed_profile.rst",
    "content": "InstalledProfile\n================\n\n.. uml:: /_static/uml/models/InstalledProfile.plantuml\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: InstalledProfile\n    :members:\n"
  },
  {
    "path": "doc/internal/core/models/organization.rst",
    "content": ".. _model-organization:\n\nOrganization\n============\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: Organization\n    :members:\n"
  },
  {
    "path": "doc/internal/core/models/profile.rst",
    "content": "Profile\n=======\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: Profile\n    :members:\n\n"
  },
  {
    "path": "doc/internal/core/models/rsa_private_key.rst",
    "content": "PrivateKey\n==========\n\n.. py:currentmodule:: commandment.models\n.. autoclass:: RSAPrivateKey\n    :members:\n\n\n"
  },
  {
    "path": "doc/internal/core/signals.rst",
    "content": "Signals\n=======\n\n.. automodule:: commandment.signals\n    :members:\n"
  },
  {
    "path": "doc/internal/decorators.rst",
    "content": "Decorators\n==========\n\n\ncommandment.mdm.parse_plist_input_data\n\ndevice_cert_check\n"
  },
  {
    "path": "doc/internal/dep/dep.rst",
    "content": "DEP Client\n==========\n\nThe main DEP API wrapper class\n\n.. autoclass:: commandment.dep.dep.DEP\n    :members:\n\n"
  },
  {
    "path": "doc/internal/dep/index.rst",
    "content": "DEP - Device Enrollment Programme\n=================================\n\nDetails of the **commandment.dep** package\n\n.. toctree::\n    :maxdepth: 2\n\n    dep\n    types\n    models\n"
  },
  {
    "path": "doc/internal/dep/models.rst",
    "content": "SQLAlchemy Models\n=================\n\n.. autoclass:: commandment.dep.models.DEPAnchorCertificate\n    :members:\n\n.. autoclass:: commandment.dep.models.DEPSupervisionCertificate\n    :members:\n\n.. autoclass:: commandment.dep.models.DEPServerTokenCertificate\n    :members:\n\n.. autoclass:: commandment.dep.models.DEPConfiguration\n    :members:\n\n.. autoclass:: commandment.dep.models.DEPProfile\n    :members:\n"
  },
  {
    "path": "doc/internal/dep/types.rst",
    "content": "DEP Types\n=========\n\n.. automodule:: commandment.dep\n    :members:\n\n"
  },
  {
    "path": "doc/internal/enroll/app.rst",
    "content": "Enrollment Blueprint\n====================\n\n.. qrefflask:: commandment:create_app()\n    :blueprints: enroll_app\n    :endpoints:\n\n.. autoflask:: commandment:create_app()\n    :blueprints: enroll_app\n    :endpoints:\n"
  },
  {
    "path": "doc/internal/enroll/index.rst",
    "content": "Enrollment\n==========\n\nDetails of the **commandment.enroll** package.\n\nThis package implements all of the non-dep enrollment logic.\n\n.. toctree::\n    :maxdepth: 2\n\n    app\n\n\n"
  },
  {
    "path": "doc/internal/flask/configuration.rst",
    "content": "Configuration Blueprint\n=======================\n\n.. autoflask:: commandment:create_app()\n    :blueprints: configuration_app\n    :endpoints:\n\n\n"
  },
  {
    "path": "doc/internal/flask/index.rst",
    "content": "Flask Endpoints\n===============\n\nThis should contain only endpoints that are not REST endpoints.\n\n.. toctree::\n    :maxdepth: 2\n\n    configuration\n    enroll\n    mdm_app\n\n"
  },
  {
    "path": "doc/internal/index.rst",
    "content": "###################\nInternals Reference\n###################\n\n.. toctree::\n    :maxdepth: 2\n\n    core/index\n\n    api/index\n    cms/index\n    dep/index\n    enroll/index\n    mdm/index\n    vpp/index\n\n    workers/index\n\n    push.rst\n\n\n    "
  },
  {
    "path": "doc/internal/mdm/app.rst",
    "content": "MDM Blueprint\n=============\n\n.. autoflask:: commandment:create_app()\n    :blueprints: mdm_app\n    :endpoints:\n\n\n"
  },
  {
    "path": "doc/internal/mdm/handlers.rst",
    "content": "MDM Command Response Handlers\n=============================\n\n.. automodule:: commandment.mdm.handlers\n    :members:\n"
  },
  {
    "path": "doc/internal/mdm/index.rst",
    "content": "MDM\n===\n\nDetails of the **commandment.mdm** package.\n\nThis package implements the Apple MDM Protocol.\nResponses and requests from devices may be generated or handled by other modules also.\n\n.. toctree::\n    :maxdepth: 2\n\n    app\n    handlers\n    types\n\n\n"
  },
  {
    "path": "doc/internal/mdm/types.rst",
    "content": "MDM Types\n=========\n\n.. automodule:: commandment.mdm\n    :members:\n\n"
  },
  {
    "path": "doc/internal/push.rst",
    "content": "push\n====\n\n.. automodule:: commandment.push\n"
  },
  {
    "path": "doc/internal/vpp/decorators.rst",
    "content": "Decorators\n==========\n\n.. automodule:: commandment.vpp.decorators\n    :members:\n"
  },
  {
    "path": "doc/internal/vpp/enum.rst",
    "content": "VPP Types\n=========\n\n.. automodule:: commandment.vpp.enum\n    :members:\n"
  },
  {
    "path": "doc/internal/vpp/errors.rst",
    "content": "VPP Errors\n==========\n\n.. automodule:: commandment.vpp.errors\n    :members:\n"
  },
  {
    "path": "doc/internal/vpp/index.rst",
    "content": "VPP - Volume Purchasing Programme\n=================================\n\nDetails of the **commandment.vpp** package.\n\nThis package implements all of the functionality related to Apple's **Volume Purchase Programme**.\n\n.. toctree::\n    :maxdepth: 2\n\n    decorators\n    enum\n    errors\n    operations\n    vpp\n"
  },
  {
    "path": "doc/internal/vpp/operations.rst",
    "content": "VPP License Operations\n======================\n\n.. autoclass:: commandment.vpp.vpp.VPPLicenseOperation\n    :members:\n\n.. autoclass:: commandment.vpp.vpp.VPPUserLicenseOperation\n    :members:\n\n.. autoclass:: commandment.vpp.vpp.VPPDeviceLicenseOperation\n    :members:\n\n"
  },
  {
    "path": "doc/internal/vpp/vpp.rst",
    "content": "VPP Client\n==========\n\nThe main VPP API wrapper class\n\n.. autoclass:: commandment.vpp.vpp.VPP\n    :members:\n\n"
  },
  {
    "path": "doc/internal/workers/index.rst",
    "content": "Worker Threads\n==============\n\n.. toctree::\n    :maxdepth: 2\n\n    runner"
  },
  {
    "path": "doc/internal/workers/runner.rst",
    "content": "runner\n======\n\n.. automodule:: commandment.runner\n"
  },
  {
    "path": "doc/make.bat",
    "content": "@ECHO OFF\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset BUILDDIR=_build\nset ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .\nset I18NSPHINXOPTS=%SPHINXOPTS% .\nif NOT \"%PAPER%\" == \"\" (\n\tset ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%\n\tset I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%\n)\n\nif \"%1\" == \"\" goto help\n\nif \"%1\" == \"help\" (\n\t:help\n\techo.Please use `make ^<target^>` where ^<target^> is one of\n\techo.  html       to make standalone HTML files\n\techo.  dirhtml    to make HTML files named index.html in directories\n\techo.  singlehtml to make a single large HTML file\n\techo.  pickle     to make pickle files\n\techo.  json       to make JSON files\n\techo.  htmlhelp   to make HTML files and a HTML help project\n\techo.  qthelp     to make HTML files and a qthelp project\n\techo.  devhelp    to make HTML files and a Devhelp project\n\techo.  epub       to make an epub\n\techo.  epub3      to make an epub3\n\techo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\n\techo.  text       to make text files\n\techo.  man        to make manual pages\n\techo.  texinfo    to make Texinfo files\n\techo.  gettext    to make PO message catalogs\n\techo.  changes    to make an overview over all changed/added/deprecated items\n\techo.  xml        to make Docutils-native XML files\n\techo.  pseudoxml  to make pseudoxml-XML files for display purposes\n\techo.  linkcheck  to check all external links for integrity\n\techo.  doctest    to run all doctests embedded in the documentation if enabled\n\techo.  coverage   to run coverage check of the documentation if enabled\n\techo.  dummy      to check syntax errors of document sources\n\tgoto end\n)\n\nif \"%1\" == \"clean\" (\n\tfor /d %%i in (%BUILDDIR%\\*) do rmdir /q /s %%i\n\tdel /q /s %BUILDDIR%\\*\n\tgoto end\n)\n\n\nREM Check if sphinx-build is available and fallback to Python version if any\n%SPHINXBUILD% 1>NUL 2>NUL\nif errorlevel 9009 goto sphinx_python\ngoto sphinx_ok\n\n:sphinx_python\n\nset SPHINXBUILD=python -m sphinx.__init__\n%SPHINXBUILD% 2> nul\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.http://sphinx-doc.org/\n\texit /b 1\n)\n\n:sphinx_ok\n\n\nif \"%1\" == \"html\" (\n\t%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%/html.\n\tgoto end\n)\n\nif \"%1\" == \"dirhtml\" (\n\t%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.\n\tgoto end\n)\n\nif \"%1\" == \"singlehtml\" (\n\t%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.\n\tgoto end\n)\n\nif \"%1\" == \"pickle\" (\n\t%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can process the pickle files.\n\tgoto end\n)\n\nif \"%1\" == \"json\" (\n\t%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can process the JSON files.\n\tgoto end\n)\n\nif \"%1\" == \"htmlhelp\" (\n\t%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can run HTML Help Workshop with the ^\n.hhp project file in %BUILDDIR%/htmlhelp.\n\tgoto end\n)\n\nif \"%1\" == \"qthelp\" (\n\t%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can run \"qcollectiongenerator\" with the ^\n.qhcp project file in %BUILDDIR%/qthelp, like this:\n\techo.^> qcollectiongenerator %BUILDDIR%\\qthelp\\commandment.qhcp\n\techo.To view the help file:\n\techo.^> assistant -collectionFile %BUILDDIR%\\qthelp\\commandment.ghc\n\tgoto end\n)\n\nif \"%1\" == \"devhelp\" (\n\t%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished.\n\tgoto end\n)\n\nif \"%1\" == \"epub\" (\n\t%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The epub file is in %BUILDDIR%/epub.\n\tgoto end\n)\n\nif \"%1\" == \"epub3\" (\n\t%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The epub3 file is in %BUILDDIR%/epub3.\n\tgoto end\n)\n\nif \"%1\" == \"latex\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; the LaTeX files are in %BUILDDIR%/latex.\n\tgoto end\n)\n\nif \"%1\" == \"latexpdf\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tcd %BUILDDIR%/latex\n\tmake all-pdf\n\tcd %~dp0\n\techo.\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\n\tgoto end\n)\n\nif \"%1\" == \"latexpdfja\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tcd %BUILDDIR%/latex\n\tmake all-pdf-ja\n\tcd %~dp0\n\techo.\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\n\tgoto end\n)\n\nif \"%1\" == \"text\" (\n\t%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The text files are in %BUILDDIR%/text.\n\tgoto end\n)\n\nif \"%1\" == \"man\" (\n\t%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The manual pages are in %BUILDDIR%/man.\n\tgoto end\n)\n\nif \"%1\" == \"texinfo\" (\n\t%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.\n\tgoto end\n)\n\nif \"%1\" == \"gettext\" (\n\t%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The message catalogs are in %BUILDDIR%/locale.\n\tgoto end\n)\n\nif \"%1\" == \"changes\" (\n\t%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.The overview file is in %BUILDDIR%/changes.\n\tgoto end\n)\n\nif \"%1\" == \"linkcheck\" (\n\t%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Link check complete; look for any errors in the above output ^\nor in %BUILDDIR%/linkcheck/output.txt.\n\tgoto end\n)\n\nif \"%1\" == \"doctest\" (\n\t%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Testing of doctests in the sources finished, look at the ^\nresults in %BUILDDIR%/doctest/output.txt.\n\tgoto end\n)\n\nif \"%1\" == \"coverage\" (\n\t%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Testing of coverage in the sources finished, look at the ^\nresults in %BUILDDIR%/coverage/python.txt.\n\tgoto end\n)\n\nif \"%1\" == \"xml\" (\n\t%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The XML files are in %BUILDDIR%/xml.\n\tgoto end\n)\n\nif \"%1\" == \"pseudoxml\" (\n\t%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.\n\tgoto end\n)\n\nif \"%1\" == \"dummy\" (\n\t%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. Dummy builder generates no files.\n\tgoto end\n)\n\n:end\n"
  },
  {
    "path": "doc/sadisplay/models.py",
    "content": "import os\nimport codecs\nimport sadisplay\nfrom flask import Flask\n\nfrom commandment.models import db, Device, Command, InstalledApplication, InstalledCertificate, \\\n    InstalledProfile\nfrom commandment.pki.models import Certificate\n\ndummyapp = Flask(__name__)\ndb.init_app(dummyapp)\n\nUML_PATH = os.path.realpath(os.path.dirname(__file__) + '/../_static/uml/models')\n\nclasses = [Certificate, Command, InstalledApplication, InstalledApplication, InstalledCertificate, InstalledProfile]\n\nwith dummyapp.app_context():\n    for cls in classes:\n        desc = sadisplay.describe(\n            [getattr(cls, attr) for attr in dir(cls)],\n            show_methods=True,\n            show_properties=True,\n            show_indexes=True,\n        )\n\n        with codecs.open(os.path.join(UML_PATH, '{}.plantuml'.format(cls.__name__)), 'w', encoding='utf-8') as f:\n            f.write(sadisplay.plantuml(desc))\n"
  },
  {
    "path": "doc/user/configuration.rst",
    "content": "Configuration\n=============\n\nAn example configuration `is provided <https://github.com/cmdmnt/commandment/blob/master/settings.cfg.example>`_ with\nthe source code.\n\nIt is recommended to copy this file to your own ``settings.cfg``, and make modifications to that file.\n\nWhen commandment runs, it will expect an environment variable ``COMMANDMENT_SETTINGS``, that contains the full path\nto the settings file.\n\nDatabase Connection\n-------------------\n\ncommandment uses SQLAlchemy as its database connection API. For more information about available configuration variables\nsee `Flask-SQLAlchemy Configuration <http://flask-sqlalchemy.pocoo.org/2.2/config/>`_.\n\nFor a testing setup, SQLite is more than adequate, so you will only need to add this line::\n\n    SQLALCHEMY_DATABASE_URI = 'sqlite:////path/to/commandment/commandment.db'\n\nTo use a local SQLite database.\n\nSelf-Signed SSL Certificate\n---------------------------\n\nIf your SSL certificate is self signed, or signed via an untrusted enterprise CA,\nyou will need to provide it as part of the configuration.\n\nIf your CA isn't already trusted throughout all of your clients (which is typically the case when you are self-signing),\nyou will need to provide the certificate eg::\n\n    CA_CERTIFICATE=\"/path/to/CA.crt\"\n\n\nMDM Push Certificate\n--------------------\n\nIf you have both the private and public key in **PEM** format, you can simply add a single variable pointing to that\nfile::\n\n    PUSH_CERTIFICATE=\"/path/to/push.pem\"\n\nOtherwise, if you need to provide a **PKCS#12** ``.p12`` file, you will also need to specify a password::\n\n    PUSH_CERTIFICATE=\"/path/to/push.p12\"\n    PUSH_CERTIFICATE_PASSWORD = \"sekret\"\n\n\n\nNuts and Bolts\n--------------\n\n- For flask web application settings, refer to `Flask - Built In Configuration Values <http://flask.pocoo.org/docs/0.12/config/#builtin-configuration-values>`_.\n- For database settings, refer to `Flask-SQLAlchemy Configuration <http://flask-sqlalchemy.pocoo.org/2.2/config/>`_.\n"
  },
  {
    "path": "doc/user/dep.rst",
    "content": "DEP (Device Enrollment Program)\n===============================\n\nThis document outlines configuration of the DEP device syncing service.\nThis information applies to classic DEP as well as Apple School Manager and Apple Business Manager.\n\nConfiguring via the UI\n----------------------\n\n- Click on **Settings** -> **DEP Accounts**.\n- **New DEP Account**\n- Click on the **Download** button to begin downloading a Public Key.\n  You will use this to upload to Apple Business Manager [#abm]_ or Apple School Manager [#asm]_.\n- Create a new **MDM Server** in ASM or ABM, as described in the ASM Help `here <https://help.apple.com/schoolmanager/#/asm1c1be359d>`_.\n- Upload the :file:`commandment-dep.cer` file you just downloaded, using the **Upload Key** button.\n.. figure:: /_static/images/asm/upload-key.png\n   :align: right\n- Download the DEP token using the **Get Token** link.\n- Unfortunately, for now you will have to upload the token using the **curl** command as outlined in API step 5.\n\nConfiguring via API\n-------------------\n\n1. Make a *GET* request to ``/dep/certificate/download`` to download the initial DEP Public Key. The public key is\n   generated on request, and stored in the database with name ``COMMANDMENT-DEP``.\n2. Perform the manual process of Adding an ASM/ABM **MDM Server**, and uploading the certificate you retrieved in step 1.\n3. Download the DEP token from ASM/ABM, which will be a file ending in ``_smime.p7m``.\n4. Upload the file to ``/dep/stoken/upload`` as multipart/form-encoded with the file field of **file**, the equivalent\n   curl command line would be::\n\n\tcurl -F 'file=@/path/to/_smime.p7m' https://commandment.local/dep/stoken/upload\n\n5. The DEP token should be decrypted, and devices should start appearing when the next DEP sync happens or when the\n   server is restarted. For convenience, the decrypted token is provided in the result of this request as a json payload,\n   structured like so::\n\n\t{\n\t  \"access_secret\": \"AS_1234\",\n\t  \"access_token\": \"AT_1234\",\n\t  \"access_token_expiry\": \"2019-10-02T00:00:00Z\",\n\t  \"consumer_key\": \"CK_1234\",\n\t  \"consumer_secret\": \"CS_1234\"\n\t}\n\n\n.. rubric:: Footnotes\n\n.. [#abm] Apple Business Manager\n.. [#asm] Apple School Manager, available at https://school.apple.com\n\n"
  },
  {
    "path": "doc/user/index.rst",
    "content": "##################\nUser Documentation\n##################\n\n.. toctree::\n    :maxdepth: 2\n\n    preface\n    install\n    configuration\n    dep\n    \n    \n\n\n    "
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n  commandment:\n    build:\n      context: .\n      dockerfile: .docker/Dockerfile\n    image: cmdmnt/commandment:latest\n#    volumes:\n#      - \"./.docker/settings.cfg.docker:/settings.cfg\"\n#      - \"./server.crt:/etc/nginx/ssl.crt\"\n#      - \"./server.key:/etc/nginx/ssl.key\"\n    ports:\n      - \"8445:443\"\n    environment:\n      - SSL_HOSTNAME=commandment.dev\n"
  },
  {
    "path": "mypy.ini",
    "content": "[mypy]\nignore_missing_imports=True\n\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\ntestpaths = tests\nmarkers =\n    depsim: mark a test requiring depsim\n    vppsim: mark a test requiring vppsim\n    dep: mark a test requiring a live DEP account\n    vpp: mark a test requiring a live VPP account\n\n"
  },
  {
    "path": "settings.cfg.example",
    "content": "from os import path\ndirname = path.dirname(__file__)\n\n# The public facing hostname of the MDM\n# This will also be used as the self signed certificate dnsname\nPUBLIC_HOSTNAME = 'commandment.dev'\n\n# Development mode listen port\nPORT = 5443\n\n# Configure your Database URI.\n# All SQLAlchemy options are available here:\n# http://flask-sqlalchemy.pocoo.org/2.1/config/\nSQLALCHEMY_DATABASE_URI = 'sqlite:///commandment.db'\n# SQLALCHEMY_DATABASE_ECHO = True\n# SQLALCHEMY_TRACK_MODIFICATIONS = False\n\n# ---------------\n# Certificates\n# ---------------\n\n# [APNS]\n# You may supply the certificate as a pair of PEM encoded files, or as a .p12 container.\n# If you supply .p12 it will be encoded as a PEM keypair\n# -----\nPUSH_CERTIFICATE = '../push.pem'\nPUSH_KEY = '../push.key'\nPUSH_CERTIFICATE_PASSWORD = 'sekret'  # for pkcs12 only\n\n# If commandment is running in development mode, specify the path to the certificate and private key.\n# These can also be generated at start up.\n# Normally SSL should be handled by Apache/Nginx/etc.\n\n# [SSL]\n# Specify the Enterprise CA here if Apple Devices won't natively trust your CA eg. If you are using a\n# self-signed CA or Enterprise CA Certificate.\n# -----\nCA_CERTIFICATE = path.join(dirname, 'ssl', 'ca.crt')\n\n# Specify the development web server SSL certificate.\n# This only applies if you are running via the CLI or flask run\n# -----\nSSL_CERTIFICATE = path.join(dirname, 'ssl', 'server.crt')\nSSL_RSA_KEY = path.join(dirname, 'ssl', 'server.key')\n\n# If not using external storage, the path to the root directory for upload storage.\n# This should not be used in production.\n# -----\nSTORAGE_ROOT = path.join(dirname, 'storage')\n\n# -------------------------\n# SCEP via SCEPy (optional)\n# -------------------------\n\n# Directory where certs, revocation lists, serials etc will be kept\n# -----\nSCEPY_CA_ROOT = \"/path/to/ca\"\n\n# X.509 Name Attributes used to generate the CA Certificate.\n# -----\nSCEPY_CA_X509_CN = 'SCEPY-CA'\nSCEPY_CA_X509_O = 'SCEPy'\nSCEPY_CA_X509_C = 'AU'\n\n# SubjectAltName extension is always on and will use this DNSName\nSAN_DNSNAME = 'scepy.dev'\n\n# (Optional) SCEP static challenge. This will have to be part of your SCEP profile\n# -----\nSCEPY_CHALLENGE = 'sekret'\n\n# Raw data will be dumped to this directory for inspection with tools such as OpenSSL (openssl asn1parse)\n# -----\nSCEPY_DUMP_DIR = '/tmp/scepy_dump'\n\n# If the GetCACert would return a single cert, force it to use a CMS degenerate case?\n# -----\nSCEPY_FORCE_DEGENERATE_FOR_SINGLE_CERT = False\n\n# ------------------------\n# Authlib (Authentication)\n# ------------------------\n\n# Token Expiry\n#\n# OAUTH2_TOKEN_EXPIRES_IN = {\n#     'authorization_code': 864000,\n#     'implicit': 3600,\n#     'password': 864000,\n#     'client_credentials': 864000\n# }\n\n"
  },
  {
    "path": "setup.cfg",
    "content": "[aliases]\ntest=pytest\n\n[tool:pytest]\npython_files=tests/*.py"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup, find_packages\nsetup(\n    name=\"commandment\",\n    version=\"0.1\",\n    description=\"Commandment is an Open Source Apple MDM server with support for managing iOS and macOS devices\",\n    packages=['commandment'],\n    include_package_data=True,\n    author=\"mosen\",\n    license=\"MIT\",\n    url=\"https://github.com/cmdmnt/commandment\",\n    classifiers=[\n        'Development Status :: 3 - Alpha',\n        'License :: OSI Approved :: MIT License',\n        'Programming Language :: Python :: 3.6'\n    ],\n    keywords='MDM',\n    install_requires=[\n        'acme==0.34.2',\n        'alembic==1.0.10',\n        'apns2-client==0.5.4',\n        'asn1crypto==0.24.0',\n        'authlib==0.11',\n        'biplist==1.0.3',\n        'blinker>=1.4',\n        'cryptography==2.6.1',\n        'flask==1.0.3',\n        'flask-alembic==2.0.1',\n        'flask-cors==3.0.4',\n        'flask-jwt==0.3.2',\n        'flask-marshmallow==0.10.1',\n        'flask-rest-jsonapi==0.29.0',\n        'flask-sqlalchemy==2.4.0',\n        'marshmallow==2.18.0',\n        'marshmallow-enum==1.4.1',\n        'marshmallow-jsonapi==0.21.0',\n        'marshmallow-sqlalchemy==0.16.3',\n        'oscrypto==0.19.1',\n        'passlib==1.7.1',\n        'requests==2.22.0',\n        'semver',\n        'sqlalchemy==1.3.3',\n        'typing==3.6.4'\n    ],\n    python_requires='>=3.6',\n    tests_require=[\n        'factory-boy==2.10.0',\n        'faker==0.8.10',\n        'mock==2.0.0',\n        'mypy==0.560'\n        'pytest==3.4.0',\n        'pytest-runner==3.0'\n    ],\n    extras_requires={\n        'ReST': [\n            'sphinx-rtd-theme',\n            'guzzle-sphinx-theme',\n            'sadisplay==0.4.8',\n            'sphinx==1.7.0b2',\n            'sphinxcontrib-httpdomain==1.6.0',\n            'sphinxcontrib-napoleon==0.6.1',\n            'sphinxcontrib-plantuml==0.10',\n        ],\n        'macOS': [\n            'pyobjc'\n        ]\n    },\n    setup_requires=['pytest-runner'],\n    entry_points={\n        'console_scripts': [\n            'commandment=commandment.cli:server',\n            'appmanifest=commandment.pkg.appmanifest:main',\n        ]\n    },\n    zip_safe=False\n)\n\n\n"
  },
  {
    "path": "testdata/Authenticate/10.11.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>BuildVersion</key>\n        <string>15G1004</string>\n        <key>Challenge</key>\n        <data>\n            YXBwbGU=\n        </data>\n        <key>DeviceName</key>\n        <string>micromdm-test</string>\n        <key>MessageType</key>\n        <string>Authenticate</string>\n        <key>Model</key>\n        <string>iMac15,1</string>\n        <key>ModelName</key>\n        <string>iMac</string>\n        <key>OSVersion</key>\n        <string>10.11.6</string>\n        <key>ProductName</key>\n        <string>iMac15,1</string>\n        <key>SerialNumber</key>\n        <string>C00000000004</string>\n        <key>Topic</key>\n        <string>com.apple.mgmt.test.00000000-1111-2222-3333-444455556666</string>\n        <key>UDID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/Authenticate/10.12.2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n        \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>BuildVersion</key>\n        <string>16C67</string>\n        <key>Challenge</key>\n        <data>YXBwbGU=</data>\n        <key>DeviceName</key>\n        <string>commandment</string>\n        <key>MessageType</key>\n        <string>Authenticate</string>\n        <key>Model</key>\n        <string>iMac17,1</string>\n        <key>ModelName</key>\n        <string>iMac</string>\n        <key>OSVersion</key>\n        <string>10.12.2</string>\n        <key>ProductName</key>\n        <string>iMac17,1</string>\n        <key>SerialNumber</key>\n        <string>000000000000</string>\n        <key>Topic</key>\n        <string>com.apple.mgmt.commandment.dev</string>\n        <key>UDID</key>\n        <string>E3568F17-92ED-450A-8904-C3BF4CB7E9A5</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/Authenticate/IOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>BuildVersion</key>\n\t<string>15E302</string>\n\t<key>MessageType</key>\n\t<string>Authenticate</string>\n\t<key>OSVersion</key>\n\t<string>11.3.1</string>\n\t<key>ProductName</key>\n\t<string>iPad4,1</string>\n\t<key>SerialNumber</key>\n\t<string>XXXXXXXXXXXX</string>\n\t<key>Topic</key>\n\t<string>com.apple.mgmt.XServer.1c111c11-1c11-1c11-1c11-1c111c111c11</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>"
  },
  {
    "path": "testdata/Authenticate/IOS-9.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>BuildVersion</key>\n\t<string>13F69</string>\n\t<key>MessageType</key>\n\t<string>Authenticate</string>\n\t<key>OSVersion</key>\n\t<string>9.3.2</string>\n\t<key>ProductName</key>\n\t<string>iPad4,1</string>\n\t<key>SerialNumber</key>\n\t<string>XXXXXXXXXXXX</string>\n\t<key>Topic</key>\n\t<string>io.micromdm.topic.00000000-1111-2222-3333-444455556666</string>\n\t<key>UDID</key>\n\t<string>1111111111111111111111111111111111111111</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/Authenticate/iOS-11.3.1-cell.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n        \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>BuildVersion</key>\n        <string>15E302</string>\n        <key>IMEI</key>\n        <string>11 111111 111111 1</string>\n        <key>MEID</key>\n        <string>1111111111111</string>\n        <key>MessageType</key>\n        <string>Authenticate</string>\n        <key>OSVersion</key>\n        <string>11.3.1</string>\n        <key>ProductName</key>\n        <string>iPhone7,2</string>\n        <key>SerialNumber</key>\n        <string>XXXXXXXXXXXX</string>\n        <key>Topic</key>\n        <string>com.apple.mgmt.XServer.1c111c11-1c11-1c11-1c11-1c111c111c11</string>\n        <key>UDID</key>\n        <string>1111111111111111111111111111111111111111</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/AvailableOSUpdates/10.12.5.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>AvailableOSUpdates</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>XProtectPlistConfigData</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<true/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/14/03/091-09590/oq7k627iuqlyoe0aceifh4uqugpp5db7pm/XProtectPlistConfigData.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-09590</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>1.0</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>MRTConfigData</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<true/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/14/01/091-14577/6cg68lk6jg3dqkkoiea8bq5vmrg9y4lid5/MRTConfigData.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-14577</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>1.0</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>Gatekeeper Configuration Data</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<true/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/22/20/091-16180/5zs9vcfyfv0aszvsv4numivit8nr636usg/GatekeeperConfigData.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-16180</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>112</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>Chinese Word List Update</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<true/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/49/37/091-16458/9nl6q1ygvhew2ip8hlumsd8oolgquxc8vp/ChineseWordlistUpdate.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-16458</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>5.26</string>\n\t\t</dict>\n\t</array>\n\t<key>CommandUUID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n\t<key>RequestType</key>\n\t<string>AvailableOSUpdates</string>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/AvailableOSUpdates/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>AvailableOSUpdates</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<false/>\n\t\t\t<key>Build</key>\n\t\t\t<string>15F79</string>\n\t\t\t<key>DownloadSize</key>\n\t\t\t<integer>225236247</integer>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>iOS 11.4</string>\n\t\t\t<key>InstallSize</key>\n\t\t\t<integer>537395200</integer>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>iOSUpdate15F79</string>\n\t\t\t<key>ProductName</key>\n\t\t\t<string>iOS</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<true/>\n\t\t\t<key>Version</key>\n\t\t\t<string>11.4</string>\n\t\t</dict>\n\t</array>\n\t<key>CommandUUID</key>\n\t<string>8fb53ef6-5fcd-46c2-bf05-ee502406f240</string>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/AvailableOSUpdates/macOS-10.13.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>AvailableOSUpdates</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>Security Update 2017-001</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<true/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/16/13/091-51303/nay02ahjmx7ksv7y3siwy02rlwfxbkltv3/macOSUpd10.13.1Supplemental.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-51303</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string> </string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>XProtectPlistConfigData</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<true/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/52/53/091-60868/g76fwkprfxgubzoa7ugaphfx5iutbek52z/XProtectPlistConfigData.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-60868</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>2099</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>Gatekeeper Configuration Data</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<true/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/21/42/091-86646/nkbr84bbslxj8pyy2lsaxrveryeevomjcu/GatekeeperConfigData.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-86646</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>140</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>macOS High Sierra 10.13.5 Update</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/41/40/091-86782/frzvpm2pwu5997tia30oepu729x87hm8jp/macOSUpdCombo10.13.5Auto.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-86782</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<true/>\n\t\t\t<key>Version</key>\n\t\t\t<string> </string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>MRTConfigData</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<true/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/27/47/091-89184/gohwwfmyzg8bpx345nxcfkmtk8nnmtfffx/MRTConfigData.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-89184</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>1.35</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array/>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>Gatekeeper Configuration Data</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<true/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/25/03/091-92948/8dnb4p6sa45djtrj7gdz97x87zxog1k1yc/GatekeeperConfigData.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>091-92948</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>144</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AllowsInstallLater</key>\n\t\t\t<true/>\n\t\t\t<key>AppIdentifiersToClose</key>\n\t\t\t<array>\n\t\t\t\t<string>com.apple.iPhoto</string>\n\t\t\t\t<string>com.apple.Aperture</string>\n\t\t\t\t<string>com.apple.dt.Xcode</string>\n\t\t\t\t<string>com.apple.PurpleRestore</string>\n\t\t\t\t<string>com.apple.iTunes</string>\n\t\t\t\t<string>com.apple.AppleConfigurationUtility</string>\n\t\t\t\t<string>com.apple.configurator</string>\n\t\t\t</array>\n\t\t\t<key>HumanReadableName</key>\n\t\t\t<string>iTunes</string>\n\t\t\t<key>HumanReadableNameLocale</key>\n\t\t\t<string>en</string>\n\t\t\t<key>IsConfigDataUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>IsCritical</key>\n\t\t\t<false/>\n\t\t\t<key>IsFirmwareUpdate</key>\n\t\t\t<false/>\n\t\t\t<key>MetadataURL</key>\n\t\t\t<string>http://swcdn.apple.com/content/downloads/01/56/zzzz091-81933/uoyng1yndy3zayq9i3jr1abrfwgehhfnxp/iTunesX.smd</string>\n\t\t\t<key>ProductKey</key>\n\t\t\t<string>zzzz091-81933</string>\n\t\t\t<key>RestartRequired</key>\n\t\t\t<false/>\n\t\t\t<key>Version</key>\n\t\t\t<string>12.7.5</string>\n\t\t</dict>\n\t</array>\n\t<key>CommandUUID</key>\n\t<string>a6121a90-4100-4928-93d8-6645722bfda7</string>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/CertificateList/10.11.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>CertificateList</key>\n        <array>\n            <dict>\n                <key>CommonName</key>\n                <string>com.apple.systemdefault</string>\n                <key>Data</key>\n                <data>\n                    MIICFDCCAX2gAwIBAgIEMshmtjALBgkqhkiG9w0BAQUwPDEgMB4G\n                    A1UEAwwXY29tLmFwcGxlLnN5c3RlbWRlZmF1bHQxGDAWBgNVBAoM\n                    D1N5c3RlbSBJZGVudGl0eTAeFw0xNTAzMDgyMjM4NDBaFw0zNTAz\n                    MDMyMjM4NDBaMDwxIDAeBgNVBAMMF2NvbS5hcHBsZS5zeXN0ZW1k\n                    ZWZhdWx0MRgwFgYDVQQKDA9TeXN0ZW0gSWRlbnRpdHkwgZ8wDQYJ\n                    KoZIhvcNAQEBBQADgY0AMIGJAoGBALWpKmld573u/zaPBwCuAMSy\n                    SxwUqQsTCi8TxPIbDLSssMkDJMlcukB9zpDkVZnP49uZow7TGE6t\n                    VuXKDmcx6gfskwkdyra05X2xNZACopdbV2OSjv87hh2yMRcmq+tt\n                    ao/3L3Ynp2ZWTVFIfgcJTHzkIKFLRwWfmEV0DE1WYNMpAgMBAAGj\n                    JTAjMAsGA1UdDwQEAwIEsDAUBgNVHSUEDTALBgkqhkiG92NkBAQw\n                    DQYJKoZIhvcNAQEFBQADgYEAjN23bV17Xk9+om0NVMhcb1dou3E0\n                    bVfMuvpYx6xcClP8Im9gGaIt8sHQPx2sRfZ0EHIPAWIDdtg8Qun3\n                    cIOLal4LigmgZEgU2V+JLyFRI7ps9QVDfbM0/So0j/B5VvUH+K8h\n                    ZTM0Y7Dg0GVwQg2tP2Fg7xFjnKz/6AO0n03Im44=\n                </data>\n                <key>IsIdentity</key>\n                <true/>\n            </dict>\n            <dict>\n                <key>CommonName</key>\n                <string>Apple Worldwide Developer Relations Certification Authority</string>\n                <key>Data</key>\n                <data>\n                    MIIEIjCCAwqgAwIBAgIIAd68xDltoBAwDQYJKoZIhvcNAQEFBQAw\n                    YjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAk\n                    BgNVBAsTHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYw\n                    FAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTEzMDIwNzIxNDg0N1oX\n                    DTIzMDIwNzIxNDg0N1owgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQK\n                    DApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3JsZHdpZGUg\n                    RGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29y\n                    bGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlv\n                    biBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n                    AoIBAQDKOFSmy1aqyCQ5SOmM7uxfuH8mkbw0U3rOfGOAYXdkXqUH\n                    I7Y5/lAtFVZYcC1+xG7BSoU+L/DehBqhV8mvexj/avoVEkkVCBms\n                    qtsqMu2WY2hSFT2Miuy/axiV4AOsAX2XBWfODoWVN2rtCbauZ81R\n                    ZJ/GXNG8V25nNYB2NqSHgW44j9grFU57Jdhav06DwY3Sk9UacbVg\n                    nJ0zTlX5ElgMhrgWDcHld0WNUEi6Ky3klIXh6MSdxmilsKP8Z35w\n                    ugJZS3dCkTm59c3hTO/AO0iMpuUhXf1qarunFjVg0uat80YpyejD\n                    i+l5wGphZxWy8P3laLxiX27Pmd3vG2P+kmWrAgMBAAGjgaYwgaMw\n                    HQYDVR0OBBYEFIgnFwmpthhgi+zruvZHWcVSVKO3MA8GA1UdEwEB\n                    /wQFMAMBAf8wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/\n                    CF4wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDovL2NybC5hcHBsZS5j\n                    b20vcm9vdC5jcmwwDgYDVR0PAQH/BAQDAgGGMBAGCiqGSIb3Y2QG\n                    AgEEAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQBPz+9Zviz1smwvj+4T\n                    hzLoBTWobot9yWkMudkXvHcs1Gfi/ZptOllc34MBvbKuKmFysa/N\n                    w0Uwj6ODDc4dR7Txk4qjdJukw5hyhzs+r0ULklS5MruQGFNrCk4Q\n                    ttkdUGwhgAqJTleMa1s8Pab93vcNIx0LSiaHP7qRkkykGRIZbVf1\n                    eliHe2iK5IaMSuviSRSqpd1VAKmuu0swruGgsbwpgOYJd+W+NKIB\n                    yn/c4grmO7i77LpilfMFY0GCzQ87HUyVpNur+cmV6U/kTecmmYHp\n                    vPm0KdIBembhLoz2IYrF+Hjhga6/05Cdqa3zr/04GpZnMBxRpVzs\n                    cYqCtGwPDBUf\n                </data>\n                <key>IsIdentity</key>\n                <false/>\n            </dict>\n        </array>\n        <key>CommandUUID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n        <key>RequestType</key>\n        <string>CertificateList</string>\n        <key>Status</key>\n        <string>Acknowledged</string>\n        <key>UDID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n    </dict>\n</plist>\n"
  },
  {
    "path": "testdata/CertificateList/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CertificateList</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CommonName</key>\n\t\t\t<string>COMMON-NAME</string>\n\t\t\t<key>Data</key>\n\t\t\t<data>Base64=\n\t\t\t</data>\n\t\t\t<key>IsIdentity</key>\n\t\t\t<false/>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>CommonName</key>\n\t\t\t<string>device-identity</string>\n\t\t\t<key>Data</key>\n\t\t\t<data>Base64=\n\t\t\t</data>\n\t\t\t<key>IsIdentity</key>\n\t\t\t<true/>\n\t\t</dict>\n\t</array>\n\t<key>CommandUUID</key>\n\t<string>506315e2-386a-44eb-9f46-e402afce7e80</string>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/CheckOut/10.11.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>MessageType</key>\n        <string>CheckOut</string>\n        <key>Topic</key>\n        <string>com.apple.mgmt.test.00000000-1111-2222-3333-444455556666</string>\n        <key>UDID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/CheckOut/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>MessageType</key>\n\t<string>CheckOut</string>\n\t<key>Topic</key>\n\t<string>com.apple.mgmt.XServer.1c111c11-1c11-1c11-1c11-1c111c111c11</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>"
  },
  {
    "path": "testdata/DeviceInformation/10.11.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>CommandUUID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n        <key>QueryResponses</key>\n        <dict>\n            <key>ActiveManagedUsers</key>\n            <array>\n                <string>00000000-1111-2222-3333-444455556666</string>\n            </array>\n            <key>AvailableDeviceCapacity</key>\n            <real>60.977592468261719</real>\n            <key>AwaitingConfiguration</key>\n            <false/>\n            <key>BluetoothMAC</key>\n            <string>00-00-00-00-00-00</string>\n            <key>BuildVersion</key>\n            <string>15G1004</string>\n            <key>CurrentConsoleManagedUser</key>\n            <string>00000000-1111-2222-3333-444455556666</string>\n            <key>DeviceCapacity</key>\n            <real>464.82241058349609</real>\n            <key>DeviceName</key>\n            <string>micromdm-testing</string>\n            <key>HostName</key>\n            <string>micromdm-testing.dev</string>\n            <key>Languages</key>\n            <array>\n                <string>en</string>\n            </array>\n            <key>LocalHostName</key>\n            <string>micromdm-testing</string>\n            <key>Model</key>\n            <string>iMac15,1</string>\n            <key>ModelName</key>\n            <string>iMac</string>\n            <key>OSUpdateSettings</key>\n            <dict>\n                <key>AutoCheckEnabled</key>\n                <false/>\n                <key>AutomaticAppInstallationEnabled</key>\n                <false/>\n                <key>AutomaticOSInstallationEnabled</key>\n                <false/>\n                <key>AutomaticSecurityUpdatesEnabled</key>\n                <true/>\n                <key>BackgroundDownloadEnabled</key>\n                <true/>\n                <key>CatalogURL</key>\n                <string>https://swscan.apple.com/content/catalogs/others/index-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz</string>\n                <key>IsDefaultCatalog</key>\n                <true/>\n                <key>PerformPeriodicCheck</key>\n                <true/>\n                <key>PreviousScanDate</key>\n                <date>2016-11-03T03:12:26Z</date>\n                <key>PreviousScanResult</key>\n                <integer>0</integer>\n            </dict>\n            <key>OSVersion</key>\n            <string>10.11.6</string>\n            <key>ProductName</key>\n            <string>iMac15,1</string>\n            <key>SerialNumber</key>\n            <string>C00000000004</string>\n            <key>UDID</key>\n            <string>00000000-1111-2222-3333-444455556666</string>\n            <key>WiFiMAC</key>\n            <string>00:00:00:00:00:00</string>\n            <key>iTunesStoreAccountHash</key>\n            <string>aAaAaAaAaAaAaAaAaAaAaAaAaAa=</string>\n            <key>iTunesStoreAccountIsActive</key>\n            <true/>\n        </dict>\n        <key>RequestType</key>\n        <string>DeviceInformation</string>\n        <key>Status</key>\n        <string>Acknowledged</string>\n        <key>UDID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n    </dict>\n</plist>\n"
  },
  {
    "path": "testdata/DeviceInformation/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>3ab89975-490b-4f33-80c7-0982069272ba</string>\n\t<key>QueryResponses</key>\n\t<dict>\n\t\t<key>AvailableDeviceCapacity</key>\n\t\t<real>15.468269348144531</real>\n\t\t<key>BluetoothMAC</key>\n\t\t<string>00:00:00:00:00:00</string>\n\t\t<key>BuildVersion</key>\n\t\t<string>15E302</string>\n\t\t<key>DataRoamingEnabled</key>\n\t\t<false/>\n\t\t<key>DeviceCapacity</key>\n\t\t<real>26.914535522460938</real>\n\t\t<key>DeviceName</key>\n\t\t<string>iPad</string>\n\t\t<key>IsRoaming</key>\n\t\t<false/>\n\t\t<key>Model</key>\n\t\t<string>MD786X</string>\n\t\t<key>ModelName</key>\n\t\t<string>iPad</string>\n\t\t<key>OSVersion</key>\n\t\t<string>11.3.1</string>\n\t\t<key>ProductName</key>\n\t\t<string>iPad4,1</string>\n\t\t<key>SerialNumber</key>\n\t\t<string>C00000000004</string>\n\t\t<key>SubscriberMCC</key>\n\t\t<string></string>\n\t\t<key>SubscriberMNC</key>\n\t\t<string></string>\n\t\t<key>UDID</key>\n\t\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n\t\t<key>VoiceRoamingEnabled</key>\n\t\t<false/>\n\t\t<key>WiFiMAC</key>\n\t\t<string>00:00:00:00:00:00</string>\n\t</dict>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/DeviceInformation/macOS-10.13.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>be926f7e-9cd2-465a-ba2b-b573dc4eaa7a</string>\n\t<key>QueryResponses</key>\n\t<dict>\n\t\t<key>ActiveManagedUsers</key>\n\t\t<array/>\n\t\t<key>AutoSetupAdminAccounts</key>\n\t\t<array/>\n\t\t<key>AvailableDeviceCapacity</key>\n\t\t<real>217.90493011474609</real>\n\t\t<key>BuildVersion</key>\n\t\t<string>17B48</string>\n\t\t<key>DeviceCapacity</key>\n\t\t<real>953.67411804199219</real>\n\t\t<key>DeviceName</key>\n\t\t<string>commandment</string>\n\t\t<key>HostName</key>\n\t\t<string>commandment.local</string>\n\t\t<key>Languages</key>\n\t\t<array>\n\t\t\t<string>en</string>\n\t\t</array>\n\t\t<key>LocalHostName</key>\n\t\t<string>commandment</string>\n\t\t<key>Locales</key>\n\t\t<array>\n\t\t\t<string>en_AU</string>\n\t\t\t<string>eu</string>\n\t\t\t<string>hr_BA</string>\n\t\t\t<string>en_CM</string>\n\t\t\t<string>en_BI</string>\n\t\t\t<string>rw_RW</string>\n\t\t\t<string>ast</string>\n\t\t\t<string>en_SZ</string>\n\t\t\t<string>he_IL</string>\n\t\t\t<string>ar</string>\n\t\t\t<string>uz_Arab</string>\n\t\t\t<string>en_PN</string>\n\t\t\t<string>as</string>\n\t\t\t<string>en_NF</string>\n\t\t\t<string>ks_IN</string>\n\t\t\t<string>es_KY</string>\n\t\t\t<string>rwk_TZ</string>\n\t\t\t<string>zh_Hant_TW</string>\n\t\t\t<string>en_CN</string>\n\t\t\t<string>gsw_LI</string>\n\t\t\t<string>ta_IN</string>\n\t\t\t<string>th_TH</string>\n\t\t\t<string>es_EA</string>\n\t\t\t<string>fr_GF</string>\n\t\t\t<string>ar_001</string>\n\t\t\t<string>en_RW</string>\n\t\t\t<string>tr_TR</string>\n\t\t\t<string>de_CH</string>\n\t\t\t<string>ee_TG</string>\n\t\t\t<string>en_NG</string>\n\t\t\t<string>fr_TG</string>\n\t\t\t<string>az</string>\n\t\t\t<string>fr_SC</string>\n\t\t\t<string>es_HN</string>\n\t\t\t<string>en_AG</string>\n\t\t\t<string>ru_KZ</string>\n\t\t\t<string>gsw</string>\n\t\t\t<string>dyo</string>\n\t\t\t<string>so_ET</string>\n\t\t\t<string>zh_Hant_MO</string>\n\t\t\t<string>de_BE</string>\n\t\t\t<string>nus_SS</string>\n\t\t\t<string>km_KH</string>\n\t\t\t<string>my_MM</string>\n\t\t\t<string>mgh_MZ</string>\n\t\t\t<string>ee_GH</string>\n\t\t\t<string>es_EC</string>\n\t\t\t<string>kw_GB</string>\n\t\t\t<string>rm_CH</string>\n\t\t\t<string>en_ME</string>\n\t\t\t<string>nyn</string>\n\t\t\t<string>mk_MK</string>\n\t\t\t<string>bs_Cyrl_BA</string>\n\t\t\t<string>ar_MR</string>\n\t\t\t<string>es_GL</string>\n\t\t\t<string>en_BM</string>\n\t\t\t<string>ms_Arab</string>\n\t\t\t<string>en_AI</string>\n\t\t\t<string>gl_ES</string>\n\t\t\t<string>en_PR</string>\n\t\t\t<string>ff_CM</string>\n\t\t\t<string>ne_IN</string>\n\t\t\t<string>or_IN</string>\n\t\t\t<string>khq_ML</string>\n\t\t\t<string>en_MG</string>\n\t\t\t<string>pt_TL</string>\n\t\t\t<string>en_LC</string>\n\t\t\t<string>iu_CA</string>\n\t\t\t<string>ta_SG</string>\n\t\t\t<string>jmc_TZ</string>\n\t\t\t<string>om_ET</string>\n\t\t\t<string>lv_LV</string>\n\t\t\t<string>es_US</string>\n\t\t\t<string>en_PT</string>\n\t\t\t<string>vai_Latn_LR</string>\n\t\t\t<string>en_NL</string>\n\t\t\t<string>to_TO</string>\n\t\t\t<string>cgg_UG</string>\n\t\t\t<string>en_MH</string>\n\t\t\t<string>ta</string>\n\t\t\t<string>zu_ZA</string>\n\t\t\t<string>shi_Latn_MA</string>\n\t\t\t<string>es_FK</string>\n\t\t\t<string>ar_KM</string>\n\t\t\t<string>en_AL</string>\n\t\t\t<string>brx_IN</string>\n\t\t\t<string>te</string>\n\t\t\t<string>chr_US</string>\n\t\t\t<string>yo_BJ</string>\n\t\t\t<string>fr_VU</string>\n\t\t\t<string>pa</string>\n\t\t\t<string>tg</string>\n\t\t\t<string>kea</string>\n\t\t\t<string>ksh_DE</string>\n\t\t\t<string>sw_CD</string>\n\t\t\t<string>te_IN</string>\n\t\t\t<string>fr_RE</string>\n\t\t\t<string>th</string>\n\t\t\t<string>ur_IN</string>\n\t\t\t<string>yo_NG</string>\n\t\t\t<string>ti</string>\n\t\t\t<string>es_HT</string>\n\t\t\t<string>es_GP</string>\n\t\t\t<string>guz_KE</string>\n\t\t\t<string>tk</string>\n\t\t\t<string>kl_GL</string>\n\t\t\t<string>ksf_CM</string>\n\t\t\t<string>mua_CM</string>\n\t\t\t<string>lag_TZ</string>\n\t\t\t<string>lb</string>\n\t\t\t<string>fr_TN</string>\n\t\t\t<string>es_PA</string>\n\t\t\t<string>pl_PL</string>\n\t\t\t<string>to</string>\n\t\t\t<string>hi_IN</string>\n\t\t\t<string>dje_NE</string>\n\t\t\t<string>es_GQ</string>\n\t\t\t<string>en_BR</string>\n\t\t\t<string>kok_IN</string>\n\t\t\t<string>pl</string>\n\t\t\t<string>fr_GN</string>\n\t\t\t<string>bem</string>\n\t\t\t<string>ha</string>\n\t\t\t<string>ckb</string>\n\t\t\t<string>lg</string>\n\t\t\t<string>tr</string>\n\t\t\t<string>en_PW</string>\n\t\t\t<string>en_NO</string>\n\t\t\t<string>nyn_UG</string>\n\t\t\t<string>sr_Latn_RS</string>\n\t\t\t<string>gsw_FR</string>\n\t\t\t<string>pa_Guru</string>\n\t\t\t<string>he</string>\n\t\t\t<string>qu_BO</string>\n\t\t\t<string>ps_AF</string>\n\t\t\t<string>lu_CD</string>\n\t\t\t<string>mgo_CM</string>\n\t\t\t<string>sn_ZW</string>\n\t\t\t<string>en_BS</string>\n\t\t\t<string>da</string>\n\t\t\t<string>ps</string>\n\t\t\t<string>ln</string>\n\t\t\t<string>pt</string>\n\t\t\t<string>hi</string>\n\t\t\t<string>lo</string>\n\t\t\t<string>ebu</string>\n\t\t\t<string>de</string>\n\t\t\t<string>gu_IN</string>\n\t\t\t<string>seh</string>\n\t\t\t<string>en_CX</string>\n\t\t\t<string>en_ZM</string>\n\t\t\t<string>fr_HT</string>\n\t\t\t<string>fr_GP</string>\n\t\t\t<string>pt_GQ</string>\n\t\t\t<string>lt</string>\n\t\t\t<string>lu</string>\n\t\t\t<string>es_TT</string>\n\t\t\t<string>ln_CD</string>\n\t\t\t<string>vai_Latn</string>\n\t\t\t<string>el_GR</string>\n\t\t\t<string>lv</string>\n\t\t\t<string>en_KE</string>\n\t\t\t<string>sbp</string>\n\t\t\t<string>hr</string>\n\t\t\t<string>en_CY</string>\n\t\t\t<string>es_GT</string>\n\t\t\t<string>twq_NE</string>\n\t\t\t<string>zh_Hant_HK</string>\n\t\t\t<string>kln_KE</string>\n\t\t\t<string>fr_GQ</string>\n\t\t\t<string>chr</string>\n\t\t\t<string>hu</string>\n\t\t\t<string>es_UY</string>\n\t\t\t<string>fr_CA</string>\n\t\t\t<string>ms_BN</string>\n\t\t\t<string>en_NR</string>\n\t\t\t<string>mer</string>\n\t\t\t<string>shi</string>\n\t\t\t<string>es_PE</string>\n\t\t\t<string>fr_SN</string>\n\t\t\t<string>bez</string>\n\t\t\t<string>sw_TZ</string>\n\t\t\t<string>wae_CH</string>\n\t\t\t<string>kkj</string>\n\t\t\t<string>hy</string>\n\t\t\t<string>dz_BT</string>\n\t\t\t<string>en_CZ</string>\n\t\t\t<string>teo_KE</string>\n\t\t\t<string>teo</string>\n\t\t\t<string>en_AR</string>\n\t\t\t<string>ar_JO</string>\n\t\t\t<string>yue_Hans_CN</string>\n\t\t\t<string>mer_KE</string>\n\t\t\t<string>khq</string>\n\t\t\t<string>ln_CF</string>\n\t\t\t<string>nn_NO</string>\n\t\t\t<string>es_SR</string>\n\t\t\t<string>en_MO</string>\n\t\t\t<string>ar_TD</string>\n\t\t\t<string>dz</string>\n\t\t\t<string>ses</string>\n\t\t\t<string>en_BW</string>\n\t\t\t<string>en_AS</string>\n\t\t\t<string>ar_IL</string>\n\t\t\t<string>es_BB</string>\n\t\t\t<string>bo_CN</string>\n\t\t\t<string>nnh</string>\n\t\t\t<string>teo_UG</string>\n\t\t\t<string>hy_AM</string>\n\t\t\t<string>ln_CG</string>\n\t\t\t<string>sr_Latn_BA</string>\n\t\t\t<string>en_MP</string>\n\t\t\t<string>ksb_TZ</string>\n\t\t\t<string>ar_SA</string>\n\t\t\t<string>smn_FI</string>\n\t\t\t<string>ar_LY</string>\n\t\t\t<string>en_AT</string>\n\t\t\t<string>so_KE</string>\n\t\t\t<string>fr_CD</string>\n\t\t\t<string>af_NA</string>\n\t\t\t<string>en_NU</string>\n\t\t\t<string>es_PH</string>\n\t\t\t<string>en_KI</string>\n\t\t\t<string>en_JE</string>\n\t\t\t<string>lkt</string>\n\t\t\t<string>fa_IR</string>\n\t\t\t<string>pt_FR</string>\n\t\t\t<string>uz_Latn_UZ</string>\n\t\t\t<string>zh_Hans_CN</string>\n\t\t\t<string>ewo_CM</string>\n\t\t\t<string>fr_PF</string>\n\t\t\t<string>ca_IT</string>\n\t\t\t<string>es_GY</string>\n\t\t\t<string>en_BZ</string>\n\t\t\t<string>ar_KW</string>\n\t\t\t<string>pt_GW</string>\n\t\t\t<string>fr_FR</string>\n\t\t\t<string>am_ET</string>\n\t\t\t<string>en_VC</string>\n\t\t\t<string>es_DM</string>\n\t\t\t<string>fr_DJ</string>\n\t\t\t<string>fr_CF</string>\n\t\t\t<string>es_SV</string>\n\t\t\t<string>en_MS</string>\n\t\t\t<string>pt_ST</string>\n\t\t\t<string>ar_SD</string>\n\t\t\t<string>luy_KE</string>\n\t\t\t<string>gd_GB</string>\n\t\t\t<string>de_LI</string>\n\t\t\t<string>it_VA</string>\n\t\t\t<string>fr_CG</string>\n\t\t\t<string>pt_CH</string>\n\t\t\t<string>ckb_IQ</string>\n\t\t\t<string>zh_Hans_SG</string>\n\t\t\t<string>en_MT</string>\n\t\t\t<string>ha_NE</string>\n\t\t\t<string>en_ID</string>\n\t\t\t<string>ewo</string>\n\t\t\t<string>af_ZA</string>\n\t\t\t<string>os_GE</string>\n\t\t\t<string>om_KE</string>\n\t\t\t<string>nl_SR</string>\n\t\t\t<string>es_ES</string>\n\t\t\t<string>es_DO</string>\n\t\t\t<string>ar_IQ</string>\n\t\t\t<string>fr_CH</string>\n\t\t\t<string>nnh_CM</string>\n\t\t\t<string>es_SX</string>\n\t\t\t<string>es_419</string>\n\t\t\t<string>en_MU</string>\n\t\t\t<string>en_US_POSIX</string>\n\t\t\t<string>yav_CM</string>\n\t\t\t<string>luo_KE</string>\n\t\t\t<string>dua_CM</string>\n\t\t\t<string>et_EE</string>\n\t\t\t<string>en_IE</string>\n\t\t\t<string>ak_GH</string>\n\t\t\t<string>rwk</string>\n\t\t\t<string>es_CL</string>\n\t\t\t<string>kea_CV</string>\n\t\t\t<string>fr_CI</string>\n\t\t\t<string>ckb_IR</string>\n\t\t\t<string>fr_BE</string>\n\t\t\t<string>se</string>\n\t\t\t<string>en_NZ</string>\n\t\t\t<string>en_MV</string>\n\t\t\t<string>en_LR</string>\n\t\t\t<string>es_PM</string>\n\t\t\t<string>en_KN</string>\n\t\t\t<string>nb_SJ</string>\n\t\t\t<string>ha_NG</string>\n\t\t\t<string>sg</string>\n\t\t\t<string>sr_Cyrl_RS</string>\n\t\t\t<string>ru_RU</string>\n\t\t\t<string>en_ZW</string>\n\t\t\t<string>sv_AX</string>\n\t\t\t<string>si</string>\n\t\t\t<string>ga_IE</string>\n\t\t\t<string>en_VG</string>\n\t\t\t<string>ff_MR</string>\n\t\t\t<string>sk</string>\n\t\t\t<string>ky_KG</string>\n\t\t\t<string>agq_CM</string>\n\t\t\t<string>mzn</string>\n\t\t\t<string>fr_BF</string>\n\t\t\t<string>mr_IN</string>\n\t\t\t<string>en_MW</string>\n\t\t\t<string>de_AT</string>\n\t\t\t<string>az_Latn</string>\n\t\t\t<string>en_LS</string>\n\t\t\t<string>ka</string>\n\t\t\t<string>naq_NA</string>\n\t\t\t<string>sl</string>\n\t\t\t<string>sn</string>\n\t\t\t<string>sr_Latn_ME</string>\n\t\t\t<string>fr_NC</string>\n\t\t\t<string>so</string>\n\t\t\t<string>is_IS</string>\n\t\t\t<string>twq</string>\n\t\t\t<string>ig_NG</string>\n\t\t\t<string>sq</string>\n\t\t\t<string>fo_FO</string>\n\t\t\t<string>sr</string>\n\t\t\t<string>tzm</string>\n\t\t\t<string>ga</string>\n\t\t\t<string>om</string>\n\t\t\t<string>en_LT</string>\n\t\t\t<string>bas_CM</string>\n\t\t\t<string>se_NO</string>\n\t\t\t<string>ki</string>\n\t\t\t<string>nl_BE</string>\n\t\t\t<string>ar_QA</string>\n\t\t\t<string>gd</string>\n\t\t\t<string>sv</string>\n\t\t\t<string>kk</string>\n\t\t\t<string>rn_BI</string>\n\t\t\t<string>es_CO</string>\n\t\t\t<string>az_Latn_AZ</string>\n\t\t\t<string>kl</string>\n\t\t\t<string>or</string>\n\t\t\t<string>es_AG</string>\n\t\t\t<string>ca</string>\n\t\t\t<string>en_VI</string>\n\t\t\t<string>km</string>\n\t\t\t<string>os</string>\n\t\t\t<string>sw</string>\n\t\t\t<string>en_MY</string>\n\t\t\t<string>kn</string>\n\t\t\t<string>en_LU</string>\n\t\t\t<string>fr_SY</string>\n\t\t\t<string>ar_TN</string>\n\t\t\t<string>en_JM</string>\n\t\t\t<string>fr_PM</string>\n\t\t\t<string>ko</string>\n\t\t\t<string>fr_NE</string>\n\t\t\t<string>ce</string>\n\t\t\t<string>fr_MA</string>\n\t\t\t<string>gl</string>\n\t\t\t<string>ru_MD</string>\n\t\t\t<string>es_BL</string>\n\t\t\t<string>saq_KE</string>\n\t\t\t<string>ks</string>\n\t\t\t<string>fr_CM</string>\n\t\t\t<string>lb_LU</string>\n\t\t\t<string>gv_IM</string>\n\t\t\t<string>fr_BI</string>\n\t\t\t<string>en_LV</string>\n\t\t\t<string>en_KR</string>\n\t\t\t<string>es_NI</string>\n\t\t\t<string>en_GB</string>\n\t\t\t<string>kw</string>\n\t\t\t<string>nl_SX</string>\n\t\t\t<string>dav_KE</string>\n\t\t\t<string>tr_CY</string>\n\t\t\t<string>ky</string>\n\t\t\t<string>en_UG</string>\n\t\t\t<string>es_BM</string>\n\t\t\t<string>en_TC</string>\n\t\t\t<string>es_AI</string>\n\t\t\t<string>ar_EG</string>\n\t\t\t<string>fr_BJ</string>\n\t\t\t<string>gu</string>\n\t\t\t<string>es_PR</string>\n\t\t\t<string>fr_RW</string>\n\t\t\t<string>gv</string>\n\t\t\t<string>lrc_IQ</string>\n\t\t\t<string>sr_Cyrl_BA</string>\n\t\t\t<string>es_MF</string>\n\t\t\t<string>fr_MC</string>\n\t\t\t<string>cs</string>\n\t\t\t<string>bez_TZ</string>\n\t\t\t<string>es_CR</string>\n\t\t\t<string>asa_TZ</string>\n\t\t\t<string>ar_EH</string>\n\t\t\t<string>fo_DK</string>\n\t\t\t<string>ms_Arab_BN</string>\n\t\t\t<string>en_JP</string>\n\t\t\t<string>sbp_TZ</string>\n\t\t\t<string>en_IL</string>\n\t\t\t<string>lt_LT</string>\n\t\t\t<string>mfe</string>\n\t\t\t<string>en_GD</string>\n\t\t\t<string>es_LC</string>\n\t\t\t<string>cy</string>\n\t\t\t<string>ug_CN</string>\n\t\t\t<string>ca_FR</string>\n\t\t\t<string>es_BO</string>\n\t\t\t<string>en_SA</string>\n\t\t\t<string>fr_BL</string>\n\t\t\t<string>bn_IN</string>\n\t\t\t<string>uz_Cyrl_UZ</string>\n\t\t\t<string>lrc_IR</string>\n\t\t\t<string>az_Cyrl</string>\n\t\t\t<string>en_IM</string>\n\t\t\t<string>sw_KE</string>\n\t\t\t<string>en_SB</string>\n\t\t\t<string>pa_Arab</string>\n\t\t\t<string>ur_PK</string>\n\t\t\t<string>haw_US</string>\n\t\t\t<string>ar_SO</string>\n\t\t\t<string>en_IN</string>\n\t\t\t<string>fil</string>\n\t\t\t<string>fr_MF</string>\n\t\t\t<string>en_WS</string>\n\t\t\t<string>es_CU</string>\n\t\t\t<string>es_BQ</string>\n\t\t\t<string>ja_JP</string>\n\t\t\t<string>fy_NL</string>\n\t\t\t<string>en_SC</string>\n\t\t\t<string>yue_Hant_HK</string>\n\t\t\t<string>en_IO</string>\n\t\t\t<string>pt_PT</string>\n\t\t\t<string>en_HK</string>\n\t\t\t<string>en_GG</string>\n\t\t\t<string>fr_MG</string>\n\t\t\t<string>de_LU</string>\n\t\t\t<string>tzm_MA</string>\n\t\t\t<string>es_BR</string>\n\t\t\t<string>en_TH</string>\n\t\t\t<string>en_SD</string>\n\t\t\t<string>nds_DE</string>\n\t\t\t<string>shi_Tfng</string>\n\t\t\t<string>ln_AO</string>\n\t\t\t<string>as_IN</string>\n\t\t\t<string>en_GH</string>\n\t\t\t<string>ms_MY</string>\n\t\t\t<string>ro_RO</string>\n\t\t\t<string>jgo_CM</string>\n\t\t\t<string>es_CW</string>\n\t\t\t<string>dua</string>\n\t\t\t<string>en_UM</string>\n\t\t\t<string>es_BS</string>\n\t\t\t<string>en_SE</string>\n\t\t\t<string>kn_IN</string>\n\t\t\t<string>en_KY</string>\n\t\t\t<string>vun_TZ</string>\n\t\t\t<string>kln</string>\n\t\t\t<string>lrc</string>\n\t\t\t<string>en_GI</string>\n\t\t\t<string>ca_ES</string>\n\t\t\t<string>rof</string>\n\t\t\t<string>pt_CV</string>\n\t\t\t<string>kok</string>\n\t\t\t<string>pt_BR</string>\n\t\t\t<string>ar_DJ</string>\n\t\t\t<string>yi_001</string>\n\t\t\t<string>fi_FI</string>\n\t\t\t<string>zh</string>\n\t\t\t<string>es_PY</string>\n\t\t\t<string>ar_SS</string>\n\t\t\t<string>mua</string>\n\t\t\t<string>sr_Cyrl_ME</string>\n\t\t\t<string>vai_Vaii_LR</string>\n\t\t\t<string>en_001</string>\n\t\t\t<string>nl_NL</string>\n\t\t\t<string>en_TK</string>\n\t\t\t<string>si_LK</string>\n\t\t\t<string>en_SG</string>\n\t\t\t<string>fr_DZ</string>\n\t\t\t<string>ca_AD</string>\n\t\t\t<string>sv_SE</string>\n\t\t\t<string>pt_AO</string>\n\t\t\t<string>vi</string>\n\t\t\t<string>xog_UG</string>\n\t\t\t<string>xog</string>\n\t\t\t<string>en_IS</string>\n\t\t\t<string>nb</string>\n\t\t\t<string>seh_MZ</string>\n\t\t\t<string>es_AR</string>\n\t\t\t<string>sk_SK</string>\n\t\t\t<string>en_SH</string>\n\t\t\t<string>ti_ER</string>\n\t\t\t<string>nd</string>\n\t\t\t<string>az_Cyrl_AZ</string>\n\t\t\t<string>zu</string>\n\t\t\t<string>ne</string>\n\t\t\t<string>nd_ZW</string>\n\t\t\t<string>el_CY</string>\n\t\t\t<string>en_IT</string>\n\t\t\t<string>nl_BQ</string>\n\t\t\t<string>da_GL</string>\n\t\t\t<string>ja</string>\n\t\t\t<string>rm</string>\n\t\t\t<string>fr_ML</string>\n\t\t\t<string>rn</string>\n\t\t\t<string>en_VU</string>\n\t\t\t<string>rof_TZ</string>\n\t\t\t<string>ro</string>\n\t\t\t<string>ebu_KE</string>\n\t\t\t<string>ru_KG</string>\n\t\t\t<string>en_SI</string>\n\t\t\t<string>sg_CF</string>\n\t\t\t<string>mfe_MU</string>\n\t\t\t<string>nl</string>\n\t\t\t<string>brx</string>\n\t\t\t<string>bs_Latn</string>\n\t\t\t<string>fa</string>\n\t\t\t<string>zgh_MA</string>\n\t\t\t<string>en_GM</string>\n\t\t\t<string>shi_Latn</string>\n\t\t\t<string>en_FI</string>\n\t\t\t<string>nn</string>\n\t\t\t<string>en_EE</string>\n\t\t\t<string>ru</string>\n\t\t\t<string>yue</string>\n\t\t\t<string>kam_KE</string>\n\t\t\t<string>fur</string>\n\t\t\t<string>vai_Vaii</string>\n\t\t\t<string>ar_ER</string>\n\t\t\t<string>rw</string>\n\t\t\t<string>ti_ET</string>\n\t\t\t<string>ff</string>\n\t\t\t<string>luo</string>\n\t\t\t<string>fa_AF</string>\n\t\t\t<string>nl_CW</string>\n\t\t\t<string>es_MQ</string>\n\t\t\t<string>en_HR</string>\n\t\t\t<string>en_FJ</string>\n\t\t\t<string>fi</string>\n\t\t\t<string>pt_MO</string>\n\t\t\t<string>be</string>\n\t\t\t<string>en_US</string>\n\t\t\t<string>en_TO</string>\n\t\t\t<string>en_SK</string>\n\t\t\t<string>bg</string>\n\t\t\t<string>ru_BY</string>\n\t\t\t<string>it_IT</string>\n\t\t\t<string>ml_IN</string>\n\t\t\t<string>gsw_CH</string>\n\t\t\t<string>qu_EC</string>\n\t\t\t<string>fo</string>\n\t\t\t<string>sv_FI</string>\n\t\t\t<string>en_FK</string>\n\t\t\t<string>nus</string>\n\t\t\t<string>ta_LK</string>\n\t\t\t<string>vun</string>\n\t\t\t<string>sr_Latn</string>\n\t\t\t<string>es_BZ</string>\n\t\t\t<string>fr</string>\n\t\t\t<string>en_SL</string>\n\t\t\t<string>bm</string>\n\t\t\t<string>es_VC</string>\n\t\t\t<string>ar_BH</string>\n\t\t\t<string>guz</string>\n\t\t\t<string>bn</string>\n\t\t\t<string>bo</string>\n\t\t\t<string>ar_SY</string>\n\t\t\t<string>es_MS</string>\n\t\t\t<string>lo_LA</string>\n\t\t\t<string>ne_NP</string>\n\t\t\t<string>uz_Latn</string>\n\t\t\t<string>be_BY</string>\n\t\t\t<string>es_IC</string>\n\t\t\t<string>sr_Latn_XK</string>\n\t\t\t<string>ar_MA</string>\n\t\t\t<string>pa_Guru_IN</string>\n\t\t\t<string>br</string>\n\t\t\t<string>luy</string>\n\t\t\t<string>kde_TZ</string>\n\t\t\t<string>es_AW</string>\n\t\t\t<string>bs</string>\n\t\t\t<string>fy</string>\n\t\t\t<string>fur_IT</string>\n\t\t\t<string>hu_HU</string>\n\t\t\t<string>ar_AE</string>\n\t\t\t<string>en_HU</string>\n\t\t\t<string>sah_RU</string>\n\t\t\t<string>zh_Hans</string>\n\t\t\t<string>en_FM</string>\n\t\t\t<string>fr_MQ</string>\n\t\t\t<string>ko_KP</string>\n\t\t\t<string>en_150</string>\n\t\t\t<string>en_DE</string>\n\t\t\t<string>ce_RU</string>\n\t\t\t<string>en_CA</string>\n\t\t\t<string>hsb_DE</string>\n\t\t\t<string>sq_AL</string>\n\t\t\t<string>en_TR</string>\n\t\t\t<string>ro_MD</string>\n\t\t\t<string>es_VE</string>\n\t\t\t<string>tg_TJ</string>\n\t\t\t<string>fr_WF</string>\n\t\t\t<string>mt_MT</string>\n\t\t\t<string>kab</string>\n\t\t\t<string>nmg_CM</string>\n\t\t\t<string>ms_SG</string>\n\t\t\t<string>en_GR</string>\n\t\t\t<string>ru_UA</string>\n\t\t\t<string>fr_MR</string>\n\t\t\t<string>zh_Hans_MO</string>\n\t\t\t<string>de_IT</string>\n\t\t\t<string>ff_GN</string>\n\t\t\t<string>bs_Cyrl</string>\n\t\t\t<string>nds_NL</string>\n\t\t\t<string>es_KN</string>\n\t\t\t<string>sw_UG</string>\n\t\t\t<string>yue_Hans</string>\n\t\t\t<string>ko_KR</string>\n\t\t\t<string>en_DG</string>\n\t\t\t<string>bo_IN</string>\n\t\t\t<string>en_CC</string>\n\t\t\t<string>shi_Tfng_MA</string>\n\t\t\t<string>lag</string>\n\t\t\t<string>it_SM</string>\n\t\t\t<string>os_RU</string>\n\t\t\t<string>en_TT</string>\n\t\t\t<string>ms_Arab_MY</string>\n\t\t\t<string>sq_MK</string>\n\t\t\t<string>es_VG</string>\n\t\t\t<string>bem_ZM</string>\n\t\t\t<string>kde</string>\n\t\t\t<string>ar_OM</string>\n\t\t\t<string>kk_KZ</string>\n\t\t\t<string>cgg</string>\n\t\t\t<string>bas</string>\n\t\t\t<string>kam</string>\n\t\t\t<string>wae</string>\n\t\t\t<string>es_MX</string>\n\t\t\t<string>sah</string>\n\t\t\t<string>zh_Hant</string>\n\t\t\t<string>en_GU</string>\n\t\t\t<string>fr_MU</string>\n\t\t\t<string>fr_KM</string>\n\t\t\t<string>ar_LB</string>\n\t\t\t<string>en_BA</string>\n\t\t\t<string>en_TV</string>\n\t\t\t<string>sr_Cyrl</string>\n\t\t\t<string>mzn_IR</string>\n\t\t\t<string>es_VI</string>\n\t\t\t<string>dje</string>\n\t\t\t<string>kab_DZ</string>\n\t\t\t<string>fil_PH</string>\n\t\t\t<string>se_SE</string>\n\t\t\t<string>vai</string>\n\t\t\t<string>hr_HR</string>\n\t\t\t<string>bs_Latn_BA</string>\n\t\t\t<string>nl_AW</string>\n\t\t\t<string>dav</string>\n\t\t\t<string>so_SO</string>\n\t\t\t<string>ar_PS</string>\n\t\t\t<string>en_FR</string>\n\t\t\t<string>uz_Cyrl</string>\n\t\t\t<string>ff_SN</string>\n\t\t\t<string>en_BB</string>\n\t\t\t<string>ki_KE</string>\n\t\t\t<string>en_TW</string>\n\t\t\t<string>naq</string>\n\t\t\t<string>en_SS</string>\n\t\t\t<string>mg_MG</string>\n\t\t\t<string>mas_KE</string>\n\t\t\t<string>en_RO</string>\n\t\t\t<string>en_PG</string>\n\t\t\t<string>mgh</string>\n\t\t\t<string>dyo_SN</string>\n\t\t\t<string>mas</string>\n\t\t\t<string>agq</string>\n\t\t\t<string>bn_BD</string>\n\t\t\t<string>haw</string>\n\t\t\t<string>yi</string>\n\t\t\t<string>nb_NO</string>\n\t\t\t<string>da_DK</string>\n\t\t\t<string>en_DK</string>\n\t\t\t<string>saq</string>\n\t\t\t<string>ug</string>\n\t\t\t<string>cy_GB</string>\n\t\t\t<string>fr_YT</string>\n\t\t\t<string>jmc</string>\n\t\t\t<string>ses_ML</string>\n\t\t\t<string>en_PH</string>\n\t\t\t<string>de_DE</string>\n\t\t\t<string>ar_YE</string>\n\t\t\t<string>es_TC</string>\n\t\t\t<string>bm_ML</string>\n\t\t\t<string>yo</string>\n\t\t\t<string>lkt_US</string>\n\t\t\t<string>uz_Arab_AF</string>\n\t\t\t<string>jgo</string>\n\t\t\t<string>sl_SI</string>\n\t\t\t<string>pt_LU</string>\n\t\t\t<string>uk</string>\n\t\t\t<string>en_CH</string>\n\t\t\t<string>asa</string>\n\t\t\t<string>en_BD</string>\n\t\t\t<string>lg_UG</string>\n\t\t\t<string>nds</string>\n\t\t\t<string>qu_PE</string>\n\t\t\t<string>mgo</string>\n\t\t\t<string>id_ID</string>\n\t\t\t<string>en_NA</string>\n\t\t\t<string>en_GY</string>\n\t\t\t<string>zgh</string>\n\t\t\t<string>pt_MZ</string>\n\t\t\t<string>fr_LU</string>\n\t\t\t<string>dsb</string>\n\t\t\t<string>mas_TZ</string>\n\t\t\t<string>en_DM</string>\n\t\t\t<string>ta_MY</string>\n\t\t\t<string>es_GD</string>\n\t\t\t<string>en_BE</string>\n\t\t\t<string>mg</string>\n\t\t\t<string>ur</string>\n\t\t\t<string>fr_GA</string>\n\t\t\t<string>ka_GE</string>\n\t\t\t<string>nmg</string>\n\t\t\t<string>en_TZ</string>\n\t\t\t<string>eu_ES</string>\n\t\t\t<string>ar_DZ</string>\n\t\t\t<string>id</string>\n\t\t\t<string>so_DJ</string>\n\t\t\t<string>hsb</string>\n\t\t\t<string>yav</string>\n\t\t\t<string>mk</string>\n\t\t\t<string>pa_Arab_PK</string>\n\t\t\t<string>ml</string>\n\t\t\t<string>en_ER</string>\n\t\t\t<string>ig</string>\n\t\t\t<string>se_FI</string>\n\t\t\t<string>mn</string>\n\t\t\t<string>ksb</string>\n\t\t\t<string>uz</string>\n\t\t\t<string>vi_VN</string>\n\t\t\t<string>ii</string>\n\t\t\t<string>qu</string>\n\t\t\t<string>en_PK</string>\n\t\t\t<string>ee</string>\n\t\t\t<string>ast_ES</string>\n\t\t\t<string>yue_Hant</string>\n\t\t\t<string>mr</string>\n\t\t\t<string>ms</string>\n\t\t\t<string>en_ES</string>\n\t\t\t<string>ha_GH</string>\n\t\t\t<string>it_CH</string>\n\t\t\t<string>sq_XK</string>\n\t\t\t<string>mt</string>\n\t\t\t<string>en_CK</string>\n\t\t\t<string>br_FR</string>\n\t\t\t<string>en_BG</string>\n\t\t\t<string>es_GF</string>\n\t\t\t<string>tk_TM</string>\n\t\t\t<string>sr_Cyrl_XK</string>\n\t\t\t<string>ksf</string>\n\t\t\t<string>en_SX</string>\n\t\t\t<string>bg_BG</string>\n\t\t\t<string>en_PL</string>\n\t\t\t<string>af</string>\n\t\t\t<string>el</string>\n\t\t\t<string>cs_CZ</string>\n\t\t\t<string>fr_TD</string>\n\t\t\t<string>zh_Hans_HK</string>\n\t\t\t<string>is</string>\n\t\t\t<string>ksh</string>\n\t\t\t<string>my</string>\n\t\t\t<string>mn_MN</string>\n\t\t\t<string>en</string>\n\t\t\t<string>it</string>\n\t\t\t<string>dsb_DE</string>\n\t\t\t<string>ii_CN</string>\n\t\t\t<string>eo</string>\n\t\t\t<string>iu</string>\n\t\t\t<string>en_ZA</string>\n\t\t\t<string>smn</string>\n\t\t\t<string>en_AD</string>\n\t\t\t<string>ak</string>\n\t\t\t<string>en_RU</string>\n\t\t\t<string>kkj_CM</string>\n\t\t\t<string>am</string>\n\t\t\t<string>es</string>\n\t\t\t<string>et</string>\n\t\t\t<string>uk_UA</string>\n\t\t</array>\n\t\t<key>Model</key>\n\t\t<string>MacPro6,1</string>\n\t\t<key>ModelName</key>\n\t\t<string>Mac Pro</string>\n\t\t<key>OSUpdateSettings</key>\n\t\t<dict>\n\t\t\t<key>AutoCheckEnabled</key>\n\t\t\t<false/>\n\t\t\t<key>AutomaticAppInstallationEnabled</key>\n\t\t\t<false/>\n\t\t\t<key>AutomaticOSInstallationEnabled</key>\n\t\t\t<false/>\n\t\t\t<key>AutomaticSecurityUpdatesEnabled</key>\n\t\t\t<true/>\n\t\t\t<key>BackgroundDownloadEnabled</key>\n\t\t\t<true/>\n\t\t\t<key>CatalogURL</key>\n\t\t\t<string>https://swscan.apple.com/content/catalogs/others/index-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz</string>\n\t\t\t<key>IsDefaultCatalog</key>\n\t\t\t<true/>\n\t\t\t<key>PerformPeriodicCheck</key>\n\t\t\t<true/>\n\t\t\t<key>PreviousScanDate</key>\n\t\t\t<date>2018-06-30T13:46:38Z</date>\n\t\t\t<key>PreviousScanResult</key>\n\t\t\t<integer>0</integer>\n\t\t</dict>\n\t\t<key>OSVersion</key>\n\t\t<string>10.13.1</string>\n\t\t<key>ProductName</key>\n\t\t<string>MacPro6,1</string>\n\t\t<key>SerialNumber</key>\n\t\t<string>AB123CD45G</string>\n\t\t<key>SystemIntegrityProtectionEnabled</key>\n\t\t<false/>\n\t\t<key>UDID</key>\n\t\t<string>00000000-1111-2222-3333-444455556666</string>\n\t\t<key>WiFiMAC</key>\n\t\t<string>00:00:00:00:00:00</string>\n\t</dict>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/DeviceLock/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CommandUUID</key>\n    <string>b6c8627f-bf4a-4ef4-8f40-d8673cd568c9</string>\n    <key>MessageResult</key>\n    <string>NoPasscodeSet</string>\n    <key>Status</key>\n    <string>Acknowledged</string>\n    <key>UDID</key>\n    <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/Errors/10.12.5-invalid-command.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>93a81e2a-8fbe-4e4b-b6e5-ee9148893f33</string>\n\t<key>ErrorChain</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>ErrorCode</key>\n\t\t\t<integer>97</integer>\n\t\t\t<key>ErrorDomain</key>\n\t\t\t<string>MDMClientError</string>\n\t\t\t<key>LocalizedDescription</key>\n\t\t\t<string>No \\'Identifier\\' in \\'RemoveProfile\\' command &lt;MDMClientError:97&gt;</string>\n\t\t</dict>\n\t</array>\n\t<key>RequestType</key>\n\t<string>RemoveProfile</string>\n\t<key>Status</key>\n\t<string>Error</string>\n\t<key>UDID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/Errors/10.13.6-invalid-command.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CommandUUID</key>\n    <string>bac6348e-3291-4523-a354-037ec379b738</string>\n    <key>ErrorChain</key>\n    <array>\n        <dict>\n            <key>ErrorCode</key>\n            <integer>12021</integer>\n            <key>ErrorDomain</key>\n            <string>MCMDMErrorDomain</string>\n            <key>LocalizedDescription</key>\n            <string>Unknown command: ShutdownDevice &lt;MDMClientError:91&gt;</string>\n        </dict>\n    </array>\n    <key>Status</key>\n    <string>Error</string>\n    <key>UDID</key>\n    <string>2F6DE437-7C14-5735-85B4-DC6B365BCAF1</string></dict>\n</plist>\n"
  },
  {
    "path": "testdata/Errors/error_invalid_request_type.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n\t<key>ErrorChain</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>ErrorCode</key>\n\t\t\t<integer>12021</integer>\n\t\t\t<key>ErrorDomain</key>\n\t\t\t<string>MCMDMErrorDomain</string>\n\t\t\t<key>LocalizedDescription</key>\n\t\t\t<string>“OSUpdateStatus” is not a valid request type.</string>\n\t\t\t<key>USEnglishDescription</key>\n\t\t\t<string>“OSUpdateStatus” is not a valid request type.</string>\n\t\t</dict>\n\t</array>\n\t<key>Status</key>\n\t<string>Error</string>\n\t<key>UDID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n</dict>\n</plist>"
  },
  {
    "path": "testdata/Errors/iOS-11.3.1-AvailableOSUpdatesFailure.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CommandUUID</key>\n    <string>93289d88-c8da-4732-9a7e-33dd51851b6e</string>\n    <key>ErrorChain</key>\n    <array>\n        <dict>\n            <key>ErrorCode</key>\n            <integer>2213</integer>\n            <key>ErrorDomain</key>\n            <string>DeviceManagement.error</string>\n            <key>LocalizedDescription</key>\n            <string>No update available.</string>\n        </dict>\n        <dict>\n            <key>ErrorCode</key>\n            <integer>3</integer>\n            <key>ErrorDomain</key>\n            <string>com.apple.softwareupdateservices.errors</string>\n            <key>LocalizedDescription</key>\n            <string>The operation couldn\\xe2\\x80\\x99t be completed. (com.apple.softwareupdateservices.errors error 3.)</string>\n        </dict>\n    </array>\n    <key>Status</key>\n    <string>Error</string>\n    <key>UDID</key>\n    <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/Errors/iOS-11.3.1-CommandFormatError.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>52aab5d2-6a61-4fe8-b685-44f8b56972d0</string>\n\t<key>Status</key>\n\t<string>CommandFormatError</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/Errors/iOS-11.3.1-RemoveProfile-Unmanaged.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CommandUUID</key>\n    <string>b2da2591-a2e5-45e5-8dd8-0ffb050d8407</string>\n    <key>ErrorChain</key>\n    <array>\n        <dict>\n            <key>ErrorCode</key>\n            <integer>12013</integer>\n            <key>ErrorDomain</key>\n            <string>MCMDMErrorDomain</string>\n            <key>LocalizedDescription</key>\n            <string>The profile \\xe2\\x80\\x9corg.github.cmdmnt.commandment.trust\\xe2\\x80\\x9d is not managed by MDM.</string>\n            <key>USEnglishDescription</key>\n            <string>The profile \\xe2\\x80\\x9corg.github.cmdmnt.commandment.trust\\xe2\\x80\\x9d is not managed by MDM.</string>\n        </dict>\n    </array>\n    <key>Status</key>\n    <string>Error</string>\n    <key>UDID</key>\n    <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/InstallApplication/iOS-11.3.1-alreadyprompting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CommandUUID</key>\n  <string>94ee37e2-3e03-4bdf-ad9c-a81ccc9d78e9</string>\n  <key>ErrorChain</key>\n  <array>\n    <dict>\n      <key>ErrorCode</key>\n      <integer>1407</integer>\n      <key>ErrorDomain</key>\n      <string>DeviceManagement.error</string>\n      <key>LocalizedDescription</key>\n      <string>The user is already being prompted.</string>\n    </dict>\n  </array>\n  <key>Status</key>\n  <string>Error</string>\n  <key>UDID</key>\n  <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/InstallApplication/iOS-12.1-prompting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CommandUUID</key>\n  <string>7a9df3db-d661-4c1c-aa0e-56d69c8718a5</string>\n  <key>Identifier</key>\n  <string>com.tinyspeck.slackmacgap</string>\n  <key>State</key>\n  <string>Prompting</string>\n  <key>Status</key>\n  <string>Acknowledged</string>\n  <key>UDID</key>\n  <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/InstallApplication/manifests/Microsoft_AutoUpdate-3.11.17101000.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>items</key>\n        <array>\n            <dict>\n                <key>assets</key>\n                <array>\n                    <dict>\n                        <key>kind</key>\n                        <string>software-package</string>\n                        <key>md5-size</key>\n                        <integer>10485760</integer>\n                        <key>md5s</key>\n                        <array>\n                            <string>4daa1b7740abf1ea74a019d5920d4a6a</string>\n                        </array>\n                        <key>url</key>\n                        <string>https://officecdn-microsoft-com.akamaized.net/pr/C1297A47-86C4-4C1F-97FA-950631F94777/OfficeMac/Microsoft_AutoUpdate_3.11.17101000_Updater.pkg</string>\n                    </dict>\n                </array>\n                <key>metadata</key>\n                <dict>\n                    <key>bundle-identifier</key>\n                    <string>com.microsoft.autoupdate</string>\n                    <key>bundle-version</key>\n                    <string>3.11.17101000</string>\n                    <key>items</key>\n                    <array>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.autoupdate.fba</string>\n                            <key>bundle-version</key>\n                            <string>3.11.17101000</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>Microsoft.MicrosoftAzureMobile</string>\n                            <key>bundle-version</key>\n                            <string>1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.errorreporting</string>\n                            <key>bundle-version</key>\n                            <string>15.39.17101000</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.autoupdate2</string>\n                            <key>bundle-version</key>\n                            <string>3.11.17101000</string>\n                        </dict>\n                    </array>\n                    <key>kind</key>\n                    <string>software</string>\n                    <key>sizeInBytes</key>\n                    <integer>3465086</integer>\n                    <key>title</key>\n                    <string>Microsoft AutoUpdate</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/InstallApplication/manifests/OneDrive-17.3.7078.1101.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>items</key>\n        <array>\n            <dict>\n                <key>assets</key>\n                <array>\n                    <dict>\n                        <key>kind</key>\n                        <string>software-package</string>\n                        <key>md5-size</key>\n                        <integer>10485760</integer>\n                        <key>md5s</key>\n                        <array>\n                            <string>ed75d7dff41873f8c8d4042fc240194c</string>\n                            <string>436f7896d1cc458e0302c9cfaf6797f2</string>\n                            <string>f7af904288dcbb05ec468432f754807b</string>\n                            <string>144607843c5160cb234b967df62f4ce9</string>\n                        </array>\n                        <key>url</key>\n                        <string>https://oneclient.sfx.ms/Mac/Direct/17.3.7078.1101/OneDrive.pkg</string>\n                    </dict>\n                </array>\n                <key>metadata</key>\n                <dict>\n                    <key>bundle-identifier</key>\n                    <string>com.microsoft.OneDrive</string>\n                    <key>bundle-version</key>\n                    <string>17.3.7078</string>\n                    <key>items</key>\n                    <array>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.OneDrive.FinderSync</string>\n                            <key>bundle-version</key>\n                            <string>7078.1101</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtCore</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtQuickControls2</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.MSSyncEngine</string>\n                            <key>bundle-version</key>\n                            <string>7078.1101</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.OneDrive</string>\n                            <key>bundle-version</key>\n                            <string>7078.1101</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.OneDriveLauncher</string>\n                            <key>bundle-version</key>\n                            <string>7078.1101</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>net.hockeyapp.sdk.mac</string>\n                            <key>bundle-version</key>\n                            <string>59</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.MacQtViews</string>\n                            <key>bundle-version</key>\n                            <string>1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtSvg</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.SkyDriveLauncher</string>\n                            <key>bundle-version</key>\n                            <string>7078.1101</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtQuickTemplates2</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtDBus</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.MSP2P</string>\n                            <key>bundle-version</key>\n                            <string>7078.1101</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.OneDriveUpdater</string>\n                            <key>bundle-version</key>\n                            <string>7078.1101</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtWidgets</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.PlaceholderManager</string>\n                            <key>bundle-version</key>\n                            <string>1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtPrintSupport</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.MSCommon</string>\n                            <key>bundle-version</key>\n                            <string>7078.1101</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtNetwork</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtQml</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtQuick</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtMacExtras</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>org.qt-project.QtGui</string>\n                            <key>bundle-version</key>\n                            <string>5.9.1</string>\n                        </dict>\n                    </array>\n                    <key>kind</key>\n                    <string>software</string>\n                    <key>sizeInBytes</key>\n                    <integer>31753894</integer>\n                    <key>title</key>\n                    <string>Microsoft OneDrive</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/InstallApplication/manifests/SkypeForBusinessInstaller-16.12.0.77.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>items</key>\n        <array>\n            <dict>\n                <key>assets</key>\n                <array>\n                    <dict>\n                        <key>kind</key>\n                        <string>software-package</string>\n                        <key>md5-size</key>\n                        <integer>10485760</integer>\n                        <key>md5s</key>\n                        <array>\n                            <string>953ab65252fe1fd3cadae48b64d2cecf</string>\n                            <string>a3384c74c85a27a2dd120d6d1f95cdbc</string>\n                            <string>6572835fe35aae86064b2d71e899e93e</string>\n                            <string>01f6c5e722276ca87bc28f9597611aaf</string>\n                        </array>\n                        <key>url</key>\n                        <string>http://download.microsoft.com/download/D/0/5/D055DA17-C7B8-4257-89A1-78E7BBE3833F/SkypeForBusinessInstaller-16.12.0.77.pkg</string>\n                    </dict>\n                </array>\n                <key>metadata</key>\n                <dict>\n                    <key>bundle-identifier</key>\n                    <string>com.microsoft.SkypeForBusiness</string>\n                    <key>bundle-version</key>\n                    <string>16.12.77</string>\n                    <key>items</key>\n                    <array>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.rdpkit</string>\n                            <key>bundle-version</key>\n                            <string>15.18</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.mbulocale</string>\n                            <key>bundle-version</key>\n                            <string>15.18</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.Model</string>\n                            <key>bundle-version</key>\n                            <string>1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.mbukernel</string>\n                            <key>bundle-version</key>\n                            <string>15.18</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.netlib</string>\n                            <key>bundle-version</key>\n                            <string>15.18</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>net.hockeyapp.sdk.mac</string>\n                            <key>bundle-version</key>\n                            <string>59</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.frameworks.wincrypto</string>\n                            <key>bundle-version</key>\n                            <string>15.18</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.wlmkernel</string>\n                            <key>bundle-version</key>\n                            <string>15.18</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.IPA</string>\n                            <key>bundle-version</key>\n                            <string>16.12.77</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.mbufont</string>\n                            <key>bundle-version</key>\n                            <string>15.18</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.ADAL</string>\n                            <key>bundle-version</key>\n                            <string>1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.SkypeAppKit</string>\n                            <key>bundle-version</key>\n                            <string>1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.mbuinstrument</string>\n                            <key>bundle-version</key>\n                            <string>15.18</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.autoupdate.fba</string>\n                            <key>bundle-version</key>\n                            <string>3.8.16112200</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.errorreporting</string>\n                            <key>bundle-version</key>\n                            <string>15.29.16112200</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.microsoft.autoupdate2</string>\n                            <key>bundle-version</key>\n                            <string>3.8.16112200</string>\n                        </dict>\n                    </array>\n                    <key>kind</key>\n                    <string>software</string>\n                    <key>sizeInBytes</key>\n                    <integer>35452911</integer>\n                    <key>title</key>\n                    <string>Skype for Business</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/InstallApplication/manifests/dotnet-sdk-2.0.2-osx-x64.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>items</key>\n        <array>\n            <dict>\n                <key>assets</key>\n                <array>\n                    <dict>\n                        <key>kind</key>\n                        <string>software-package</string>\n                        <key>md5-size</key>\n                        <integer>10485760</integer>\n                        <key>md5s</key>\n                        <array>\n                            <string>46fc4f5d1d6988999a697d08867dab8b</string>\n                            <string>a950d6d1df6347d1dd61c8fb28b03bf4</string>\n                            <string>0ae19fbd7fbced52c98844048ad980e9</string>\n                            <string>cbd454fbb0b6e57344a7b996f6f9eb22</string>\n                            <string>772f81302420ef668c5280ea3ad03438</string>\n                            <string>6f8a30304d85a406b3351f880f325817</string>\n                            <string>3ddb09ecfbe4bb2d371a00541947f183</string>\n                            <string>0f430c3787aadfaae152dd59d34b2f7c</string>\n                            <string>0c387e5a2ec6d722cda8080eec0e9c33</string>\n                            <string>18827156403d3b503a8621d882de759d</string>\n                            <string>4b00162e0de7068f20bc0ac2488762c2</string>\n                            <string>0769ec04e2f9793979997f151a10660e</string>\n                            <string>f01f03afd6a7561fcc8c464f67b8f524</string>\n                            <string>6caf9ffb84db47a296a267bcb2ea0447</string>\n                        </array>\n                        <key>url</key>\n                        <string>https://download.microsoft.com/download/7/3/A/73A3E4DC-F019-47D1-9951-0453676E059B/dotnet-sdk-2.0.2-osx-x64.pkg</string>\n                    </dict>\n                </array>\n                <key>metadata</key>\n                <dict>\n                    <key>bundle-identifier</key>\n                    <string>com.microsoft.dotnet.sharedframework.Microsoft.NETCore.App.2.0.0.component.osx.x64.pkg</string>\n                    <key>bundle-version</key>\n                    <string>2.0.0</string>\n                    <key>kind</key>\n                    <string>software</string>\n                    <key>sizeInBytes</key>\n                    <integer>140938719</integer>\n                    <key>title</key>\n                    <string>Microsoft .NET Core SDK - 2.0.2 (x64)</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/InstallApplication/manifests/munkitools-3.1.0.3430.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>items</key>\n        <array>\n            <dict>\n                <key>assets</key>\n                <array>\n                    <dict>\n                        <key>kind</key>\n                        <string>software-package</string>\n                        <key>md5-size</key>\n                        <integer>10485760</integer>\n                        <key>md5s</key>\n                        <array>\n                            <string>0afbe2fbe7cb81ff531834cba82f3a75</string>\n                        </array>\n                        <key>url</key>\n                        <string>https://github.com/munki/munki/releases/download/v3.1.0/munkitools-3.1.0.3430.pkg</string>\n                    </dict>\n                </array>\n                <key>metadata</key>\n                <dict>\n                    <key>bundle-identifier</key>\n                    <string>com.googlecode.munki.core</string>\n                    <key>bundle-version</key>\n                    <string>3.1.0.3430</string>\n                    <key>items</key>\n                    <array>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.googlecode.munki.MunkiStatus</string>\n                            <key>bundle-version</key>\n                            <string>3401</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.googlecode.munki.munki-notifier</string>\n                            <key>bundle-version</key>\n                            <string>3251</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.googlecode.munki.MSCDockTilePlugin</string>\n                            <key>bundle-version</key>\n                            <string>1</string>\n                        </dict>\n                        <dict>\n                            <key>bundle-identifier</key>\n                            <string>com.googlecode.munki.ManagedSoftwareCenter</string>\n                            <key>bundle-version</key>\n                            <string>3425</string>\n                        </dict>\n                    </array>\n                    <key>kind</key>\n                    <string>software</string>\n                    <key>sizeInBytes</key>\n                    <integer>3594282</integer>\n                    <key>title</key>\n                    <string>Munki - Managed software installation for OS X</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/InstalledApplicationList/10.11.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n\t<key>InstalledApplicationList</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>BundleSize</key>\n\t\t\t<integer>5855484</integer>\n\t\t\t<key>Identifier</key>\n\t\t\t<string>com.apple.systempreferences</string>\n\t\t\t<key>Name</key>\n\t\t\t<string>System Preferences</string>\n\t\t\t<key>ShortVersion</key>\n\t\t\t<string>14.0</string>\n\t\t\t<key>Version</key>\n\t\t\t<string>14.0</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>BundleSize</key>\n\t\t\t<integer>65092</integer>\n\t\t\t<key>Name</key>\n\t\t\t<string>Set Info</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>BundleSize</key>\n\t\t\t<integer>0</integer>\n\t\t\t<key>Name</key>\n\t\t\t<string>Install OS X Yosemite</string>\n\t\t</dict>\n\t</array>\n\t<key>RequestType</key>\n\t<string>InstalledApplicationList</string>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n</dict>\n</plist>"
  },
  {
    "path": "testdata/InstalledApplicationList/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>368b6b82-f89c-4af7-8a02-ed72af86116c</string>\n\t<key>InstalledApplicationList</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>AdHocCodeSigned</key>\n\t\t\t<false/>\n\t\t\t<key>AppStoreVendable</key>\n\t\t\t<true/>\n\t\t\t<key>BetaApp</key>\n\t\t\t<false/>\n\t\t\t<key>BundleSize</key>\n\t\t\t<integer>143695872</integer>\n\t\t\t<key>DeviceBasedVPP</key>\n\t\t\t<false/>\n\t\t\t<key>DynamicSize</key>\n\t\t\t<integer>192512</integer>\n\t\t\t<key>ExternalVersionIdentifier</key>\n\t\t\t<integer>827127239</integer>\n\t\t\t<key>HasUpdateAvailable</key>\n\t\t\t<false/>\n\t\t\t<key>Identifier</key>\n\t\t\t<string>com.microsoft.lync2013.iphone</string>\n\t\t\t<key>Installing</key>\n\t\t\t<false/>\n\t\t\t<key>IsValidated</key>\n\t\t\t<true/>\n\t\t\t<key>Name</key>\n\t\t\t<string>Business</string>\n\t\t\t<key>ShortVersion</key>\n\t\t\t<string>6.20.2</string>\n\t\t\t<key>Version</key>\n\t\t\t<string>6.20.2.2</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AdHocCodeSigned</key>\n\t\t\t<false/>\n\t\t\t<key>AppStoreVendable</key>\n\t\t\t<true/>\n\t\t\t<key>BetaApp</key>\n\t\t\t<false/>\n\t\t\t<key>BundleSize</key>\n\t\t\t<integer>80281600</integer>\n\t\t\t<key>DeviceBasedVPP</key>\n\t\t\t<false/>\n\t\t\t<key>DynamicSize</key>\n\t\t\t<integer>1032192</integer>\n\t\t\t<key>ExternalVersionIdentifier</key>\n\t\t\t<integer>826041298</integer>\n\t\t\t<key>HasUpdateAvailable</key>\n\t\t\t<false/>\n\t\t\t<key>Identifier</key>\n\t\t\t<string>com.agilebits.onepassword-ios</string>\n\t\t\t<key>Installing</key>\n\t\t\t<false/>\n\t\t\t<key>IsValidated</key>\n\t\t\t<true/>\n\t\t\t<key>Name</key>\n\t\t\t<string>1Password</string>\n\t\t\t<key>ShortVersion</key>\n\t\t\t<string>7.0.6</string>\n\t\t\t<key>Version</key>\n\t\t\t<string>70006002</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AdHocCodeSigned</key>\n\t\t\t<false/>\n\t\t\t<key>AppStoreVendable</key>\n\t\t\t<true/>\n\t\t\t<key>BetaApp</key>\n\t\t\t<false/>\n\t\t\t<key>BundleSize</key>\n\t\t\t<integer>220049408</integer>\n\t\t\t<key>DeviceBasedVPP</key>\n\t\t\t<false/>\n\t\t\t<key>DynamicSize</key>\n\t\t\t<integer>303104</integer>\n\t\t\t<key>ExternalVersionIdentifier</key>\n\t\t\t<integer>827416674</integer>\n\t\t\t<key>HasUpdateAvailable</key>\n\t\t\t<true/>\n\t\t\t<key>Identifier</key>\n\t\t\t<string>com.microsoft.Office.Powerpoint</string>\n\t\t\t<key>Installing</key>\n\t\t\t<false/>\n\t\t\t<key>IsValidated</key>\n\t\t\t<true/>\n\t\t\t<key>Name</key>\n\t\t\t<string>PowerPoint</string>\n\t\t\t<key>ShortVersion</key>\n\t\t\t<string>2.14</string>\n\t\t\t<key>Version</key>\n\t\t\t<string>2.14.18060400</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AdHocCodeSigned</key>\n\t\t\t<false/>\n\t\t\t<key>AppStoreVendable</key>\n\t\t\t<true/>\n\t\t\t<key>BetaApp</key>\n\t\t\t<false/>\n\t\t\t<key>BundleSize</key>\n\t\t\t<integer>17637376</integer>\n\t\t\t<key>DeviceBasedVPP</key>\n\t\t\t<false/>\n\t\t\t<key>DynamicSize</key>\n\t\t\t<integer>155648</integer>\n\t\t\t<key>ExternalVersionIdentifier</key>\n\t\t\t<integer>826584966</integer>\n\t\t\t<key>HasUpdateAvailable</key>\n\t\t\t<false/>\n\t\t\t<key>Identifier</key>\n\t\t\t<string>com.panic.Prompt2</string>\n\t\t\t<key>Installing</key>\n\t\t\t<false/>\n\t\t\t<key>IsValidated</key>\n\t\t\t<true/>\n\t\t\t<key>Name</key>\n\t\t\t<string>Prompt</string>\n\t\t\t<key>ShortVersion</key>\n\t\t\t<string>2.6.6</string>\n\t\t\t<key>Version</key>\n\t\t\t<string>86791</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>AdHocCodeSigned</key>\n\t\t\t<false/>\n\t\t\t<key>AppStoreVendable</key>\n\t\t\t<true/>\n\t\t\t<key>BetaApp</key>\n\t\t\t<false/>\n\t\t\t<key>BundleSize</key>\n\t\t\t<integer>179638272</integer>\n\t\t\t<key>DeviceBasedVPP</key>\n\t\t\t<false/>\n\t\t\t<key>DynamicSize</key>\n\t\t\t<integer>225280</integer>\n\t\t\t<key>ExternalVersionIdentifier</key>\n\t\t\t<integer>827489856</integer>\n\t\t\t<key>HasUpdateAvailable</key>\n\t\t\t<true/>\n\t\t\t<key>Identifier</key>\n\t\t\t<string>com.microsoft.onenote</string>\n\t\t\t<key>Installing</key>\n\t\t\t<false/>\n\t\t\t<key>IsValidated</key>\n\t\t\t<true/>\n\t\t\t<key>Name</key>\n\t\t\t<string>OneNote</string>\n\t\t\t<key>ShortVersion</key>\n\t\t\t<string>16.14</string>\n\t\t\t<key>Version</key>\n\t\t\t<string>16014000.18060400</string>\n\t\t</dict>\n\t</array>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/InstalledApplicationList/iOS-12.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CommandUUID</key>\n  <string>ca851cf1-e85b-4fed-bf0a-dd7a4e4859b2</string>\n  <key>InstalledApplicationList</key>\n  <array>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>87191552</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>196608</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829185909</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>com.agilebits.onepassword-ios</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>1Password</string>\n      <key>ShortVersion</key>\n      <string>7.2.2</string>\n      <key>Version</key>\n      <string>70202006</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>56799232</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>102400</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829315696</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>com.seek.jobseeker</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>SEEK Jobs</string>\n      <key>ShortVersion</key>\n      <string>2.17.0</string>\n      <key>Version</key>\n      <string>714</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>59449344</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>45056</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829166341</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>com.microsoft.azure</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>Azure</string>\n      <key>ShortVersion</key>\n      <string>1.0.33</string>\n      <key>Version</key>\n      <string>1.0.33.20181102</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>47652864</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>32768</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829257810</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>au.com.sbs.ondemand</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>On Demand</string>\n      <key>ShortVersion</key>\n      <string>2.10.3</string>\n      <key>Version</key>\n      <string>784</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>25075712</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>28672</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829313936</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>com.amazonaws.mobileConsole</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>AWS Console</string>\n      <key>ShortVersion</key>\n      <string>2.0.1</string>\n      <key>Version</key>\n      <string>500</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>39378944</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>94208</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>828862112</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>au.net.abc.ABCiView</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>ABC iview</string>\n      <key>ShortVersion</key>\n      <string>4.4.1</string>\n      <key>Version</key>\n      <string>261</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>43778048</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>45056</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829312998</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>com.safariflow.SafariQueue</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>Queue</string>\n      <key>ShortVersion</key>\n      <string>2.3.1</string>\n      <key>Version</key>\n      <string>418</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>83869696</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>413696</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829229890</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>com.tinyspeck.chatlyio</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>Slack</string>\n      <key>ShortVersion</key>\n      <string>3.57</string>\n      <key>Version</key>\n      <string>399418</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>1355776</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>3522560</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>824204014</integer>\n      <key>HasUpdateAvailable</key>\n      <false/>\n      <key>Identifier</key>\n      <string>com.fastmail.FastMail</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>FastMail</string>\n      <key>ShortVersion</key>\n      <string>1.2.7</string>\n      <key>Version</key>\n      <string>409</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>34725888</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>12288</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>811934140</integer>\n      <key>HasUpdateAvailable</key>\n      <false/>\n      <key>Identifier</key>\n      <string>com.vmware.watchlist</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>Watchlist</string>\n      <key>ShortVersion</key>\n      <string>1.4.2</string>\n      <key>Version</key>\n      <string>2556390</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>17743872</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>36864</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>828512841</integer>\n      <key>HasUpdateAvailable</key>\n      <true/>\n      <key>Identifier</key>\n      <string>com.panic.Prompt2</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>Prompt</string>\n      <key>ShortVersion</key>\n      <string>2.6.7</string>\n      <key>Version</key>\n      <string>95685</string>\n    </dict>\n    <dict>\n      <key>AdHocCodeSigned</key>\n      <false/>\n      <key>AppStoreVendable</key>\n      <true/>\n      <key>BetaApp</key>\n      <false/>\n      <key>BundleSize</key>\n      <integer>72933376</integer>\n      <key>DeviceBasedVPP</key>\n      <false/>\n      <key>DynamicSize</key>\n      <integer>12288</integer>\n      <key>ExternalVersionIdentifier</key>\n      <integer>819117024</integer>\n      <key>HasUpdateAvailable</key>\n      <false/>\n      <key>Identifier</key>\n      <string>com.zmangames.f2z.pandemic-ios</string>\n      <key>Installing</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>Name</key>\n      <string>Pandemic</string>\n      <key>ShortVersion</key>\n      <string>1.2.5</string>\n      <key>Version</key>\n      <string>1432</string>\n    </dict>\n  </array>\n  <key>Status</key>\n  <string>Acknowledged</string>\n  <key>UDID</key>\n  <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/ManagedApplicationList/iOS-11.3.1-Failed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CommandUUID</key>\n  <string>b3f87776-66a4-40f4-a194-8b451a2eacb1</string>\n  <key>ManagedApplicationList</key>\n  <dict>\n    <key>com.apple.iWork.Keynote</key>\n    <dict>\n      <key>HasConfiguration</key>\n      <false/>\n      <key>HasFeedback</key>\n      <false/>\n      <key>IsValidated</key>\n      <false/>\n      <key>ManagementFlags</key>\n      <integer>0</integer>\n      <key>Status</key>\n      <string>Failed</string>\n    </dict>\n    <key>com.tinyspeck.slackmacgap</key>\n    <dict>\n      <key>HasConfiguration</key>\n      <false/>\n      <key>HasFeedback</key>\n      <false/>\n      <key>IsValidated</key>\n      <false/>\n      <key>ManagementFlags</key>\n      <integer>0</integer>\n      <key>Status</key>\n      <string>Failed</string>\n    </dict>\n  </dict>\n  <key>Status</key>\n  <string>Acknowledged</string>\n  <key>UDID</key>\n  <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/ManagedApplicationList/iOS-12.1-Failed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>CommandUUID</key>\n        <string>7fc66d05-4bdb-41e4-9814-5a4c21e74e16</string>\n        <key>ManagedApplicationList</key>\n        <dict>\n            <key>com.apple.iWork.Keynote</key>\n            <dict>\n                <key>HasConfiguration</key>\n                <false/>\n                <key>HasFeedback</key>\n                <false/>\n                <key>IsValidated</key>\n                <false/>\n                <key>ManagementFlags</key>\n                <integer>0</integer>\n                <key>Status</key>\n                <string>Failed</string>\n            </dict>\n            <key>com.tinyspeck.slackmacgap</key>\n            <dict>\n                <key>HasConfiguration</key>\n                <false/>\n                <key>HasFeedback</key>\n                <false/>\n                <key>IsValidated</key>\n                <false/>\n                <key>ManagementFlags</key>\n                <integer>0</integer>\n                <key>Status</key>\n                <string>Failed</string>\n            </dict>\n        </dict>\n        <key>Status</key>\n        <string>Acknowledged</string>\n        <key>UDID</key>\n        <string>1c111c111c111c111c111c111c111c111c111c11</string>\n    </dict>\n</plist>\n"
  },
  {
    "path": "testdata/ManagedApplicationList/iOS-12.1-Installing.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CommandUUID</key>\n  <string>cfaa6f63-6ea9-493c-ab2e-a534937c5eda</string>\n  <key>ManagedApplicationList</key>\n  <dict>\n    <key>com.apple.Numbers</key>\n    <dict>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829165942</integer>\n      <key>HasConfiguration</key>\n      <false/>\n      <key>HasFeedback</key>\n      <false/>\n      <key>IsValidated</key>\n      <false/>\n      <key>ManagementFlags</key>\n      <integer>0</integer>\n      <key>Status</key>\n      <string>Installing</string>\n    </dict>\n  </dict>\n  <key>Status</key>\n  <string>Acknowledged</string>\n  <key>UDID</key>\n  <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/ManagedApplicationList/iOS-12.1-Managed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CommandUUID</key>\n  <string>0a40830d-1cf9-4a00-a153-d8294e576c3f</string>\n  <key>ManagedApplicationList</key>\n  <dict>\n    <key>com.apple.Numbers</key>\n    <dict>\n      <key>ExternalVersionIdentifier</key>\n      <integer>829165942</integer>\n      <key>HasConfiguration</key>\n      <false/>\n      <key>HasFeedback</key>\n      <false/>\n      <key>IsValidated</key>\n      <true/>\n      <key>ManagementFlags</key>\n      <integer>0</integer>\n      <key>Status</key>\n      <string>Managed</string>\n    </dict>\n  </dict>\n  <key>Status</key>\n  <string>Acknowledged</string>\n  <key>UDID</key>\n  <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/ManagedApplicationList/iOS-12.1-RejectedPrompting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CommandUUID</key>\n  <string>2dce0324-3a9c-493d-bd7a-2d2a2985a67e</string>\n  <key>ManagedApplicationList</key>\n  <dict>\n    <key>com.apple.iWork.Keynote</key>\n    <dict>\n      <key>HasConfiguration</key>\n      <false/>\n      <key>HasFeedback</key>\n      <false/>\n      <key>IsValidated</key>\n      <false/>\n      <key>ManagementFlags</key>\n      <integer>0</integer>\n      <key>Status</key>\n      <string>UserRejected</string>\n    </dict>\n    <key>com.tinyspeck.slackmacgap</key>\n    <dict>\n      <key>HasConfiguration</key>\n      <false/>\n      <key>HasFeedback</key>\n      <false/>\n      <key>IsValidated</key>\n      <false/>\n      <key>ManagementFlags</key>\n      <integer>0</integer>\n      <key>Status</key>\n      <string>UserRejected</string>\n    </dict>\n  </dict>\n  <key>Status</key>\n  <string>Acknowledged</string>\n  <key>UDID</key>\n  <string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/NotNow/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>bb5d7813-e7c3-4279-b954-4b678925de5f</string>\n\t<key>Status</key>\n\t<string>NotNow</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/ProfileList/10.11.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>CommandUUID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n        <key>ProfileList</key>\n        <array>\n            <dict>\n                <key>HasRemovalPasscode</key>\n                <false/>\n                <key>IsEncrypted</key>\n                <false/>\n                <key>PayloadContent</key>\n                <array>\n                    <dict>\n                        <key>PayloadDescription</key>\n                        <string>Installs the TLS certificate for MicroMDM</string>\n                        <key>PayloadDisplayName</key>\n                        <string>Self-signed TLS certificate for MicroMDM</string>\n                        <key>PayloadIdentifier</key>\n                        <string>com.github.micromdm.tls</string>\n                        <key>PayloadOrganization</key>\n                        <string></string>\n                        <key>PayloadType</key>\n                        <string>com.apple.security.pkcs1</string>\n                        <key>PayloadUUID</key>\n                        <string>4b12d46f-0cfb-4d58-ab5c-74873235b60b</string>\n                        <key>PayloadVersion</key>\n                        <integer>1</integer>\n                    </dict>\n                    <dict>\n                        <key>PayloadDescription</key>\n                        <string>Installs the root CA certificate for MicroMDM</string>\n                        <key>PayloadDisplayName</key>\n                        <string>Root certificate for MicroMDM</string>\n                        <key>PayloadIdentifier</key>\n                        <string>com.github.micromdm.ssl.ca</string>\n                        <key>PayloadOrganization</key>\n                        <string></string>\n                        <key>PayloadType</key>\n                        <string>com.apple.security.root</string>\n                        <key>PayloadUUID</key>\n                        <string>de6a8869-e0c3-4fa3-ba3e-f89001f8ee71</string>\n                        <key>PayloadVersion</key>\n                        <integer>1</integer>\n                    </dict>\n                    <dict>\n                        <key>PayloadDescription</key>\n                        <string>Enrolls with the MDM server</string>\n                        <key>PayloadDisplayName</key>\n                        <string></string>\n                        <key>PayloadIdentifier</key>\n                        <string>com.github.micromdm.mdm</string>\n                        <key>PayloadOrganization</key>\n                        <string>MicroMDM</string>\n                        <key>PayloadType</key>\n                        <string>com.apple.mdm</string>\n                        <key>PayloadUUID</key>\n                        <string>e021da61-092a-4b73-8c26-00f5fdcf7e4e</string>\n                        <key>PayloadVersion</key>\n                        <integer>1</integer>\n                    </dict>\n                    <dict>\n                        <key>PayloadDescription</key>\n                        <string>Configures SCEP</string>\n                        <key>PayloadDisplayName</key>\n                        <string>SCEP</string>\n                        <key>PayloadIdentifier</key>\n                        <string>com.github.micromdm.scep</string>\n                        <key>PayloadOrganization</key>\n                        <string>MicroMDM</string>\n                        <key>PayloadType</key>\n                        <string>com.apple.security.scep</string>\n                        <key>PayloadUUID</key>\n                        <string>519e158c-c699-42fd-8cdf-1cd5612088df</string>\n                        <key>PayloadVersion</key>\n                        <integer>1</integer>\n                    </dict>\n                </array>\n                <key>PayloadDescription</key>\n                <string>The server may alter your settings</string>\n                <key>PayloadDisplayName</key>\n                <string>Enrollment Profile</string>\n                <key>PayloadIdentifier</key>\n                <string>com.github.micromdm.micromdm.mdm</string>\n                <key>PayloadOrganization</key>\n                <string>MicroMDM</string>\n                <key>PayloadRemovalDisallowed</key>\n                <false/>\n                <key>PayloadUUID</key>\n                <string>dd3c707b-b18c-4979-bd94-2fe5cd804a47</string>\n                <key>PayloadVersion</key>\n                <integer>1</integer>\n                <key>SignerCertificates</key>\n                <array/>\n            </dict>\n        </array>\n        <key>RequestType</key>\n        <string>ProfileList</string>\n        <key>Status</key>\n        <string>Acknowledged</string>\n        <key>UDID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/ProfileList/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>2d1d3845-3e06-4687-a357-b343ebb17888</string>\n\t<key>ProfileList</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>HasRemovalPasscode</key>\n\t\t\t<false/>\n\t\t\t<key>IsEncrypted</key>\n\t\t\t<false/>\n\t\t\t<key>IsManaged</key>\n\t\t\t<false/>\n\t\t\t<key>PayloadContent</key>\n\t\t\t<array>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>PayloadDescription</key>\n\t\t\t\t\t<string>Required for your device to trust the server</string>\n\t\t\t\t\t<key>PayloadDisplayName</key>\n\t\t\t\t\t<string>Certificate Authority</string>\n\t\t\t\t\t<key>PayloadIdentifier</key>\n\t\t\t\t\t<string>dev.commandment.ca</string>\n\t\t\t\t\t<key>PayloadType</key>\n\t\t\t\t\t<string>com.apple.security.root</string>\n\t\t\t\t\t<key>PayloadVersion</key>\n\t\t\t\t\t<integer>1</integer>\n\t\t\t\t</dict>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>PayloadDescription</key>\n\t\t\t\t\t<string>Required for your device to trust the server</string>\n\t\t\t\t\t<key>PayloadDisplayName</key>\n\t\t\t\t\t<string>Web Server Certificate</string>\n\t\t\t\t\t<key>PayloadIdentifier</key>\n\t\t\t\t\t<string>dev.commandment.ssl</string>\n\t\t\t\t\t<key>PayloadType</key>\n\t\t\t\t\t<string>com.apple.security.pkcs1</string>\n\t\t\t\t\t<key>PayloadVersion</key>\n\t\t\t\t\t<integer>1</integer>\n\t\t\t\t</dict>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>PayloadDescription</key>\n\t\t\t\t\t<string>Required to identify your device to the MDM</string>\n\t\t\t\t\t<key>PayloadDisplayName</key>\n\t\t\t\t\t<string>device-identity</string>\n\t\t\t\t\t<key>PayloadIdentifier</key>\n\t\t\t\t\t<string>dev.commandment.identity</string>\n\t\t\t\t\t<key>PayloadType</key>\n\t\t\t\t\t<string>com.apple.security.pkcs12</string>\n\t\t\t\t\t<key>PayloadVersion</key>\n\t\t\t\t\t<integer>1</integer>\n\t\t\t\t</dict>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>PayloadDescription</key>\n\t\t\t\t\t<string>Enrolls your device with the MDM server</string>\n\t\t\t\t\t<key>PayloadDisplayName</key>\n\t\t\t\t\t<string>Device Configuration and Management</string>\n\t\t\t\t\t<key>PayloadIdentifier</key>\n\t\t\t\t\t<string>dev.commandment.mdm</string>\n\t\t\t\t\t<key>PayloadType</key>\n\t\t\t\t\t<string>com.apple.mdm</string>\n\t\t\t\t\t<key>PayloadVersion</key>\n\t\t\t\t\t<integer>1</integer>\n\t\t\t\t</dict>\n\t\t\t</array>\n\t\t\t<key>PayloadDescription</key>\n\t\t\t<string>Enrolls your device for Mobile Device Management</string>\n\t\t\t<key>PayloadDisplayName</key>\n\t\t\t<string>Commandment Enrollment Profile</string>\n\t\t\t<key>PayloadIdentifier</key>\n\t\t\t<string>dev.commandment.enroll</string>\n\t\t\t<key>PayloadOrganization</key>\n\t\t\t<string>Commandment Inc</string>\n\t\t\t<key>PayloadRemovalDisallowed</key>\n\t\t\t<false/>\n\t\t\t<key>PayloadUUID</key>\n\t\t\t<string>ac9e8c32-5c15-40b9-b187-70a81c49aa4e</string>\n\t\t\t<key>PayloadVersion</key>\n\t\t\t<integer>1</integer>\n\t\t</dict>\n\t</array>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/README.rst",
    "content": "Test Data\n=========\n\nThis directory contains the test fixtures.\n\nYou can also place test certificates here but they will be ignored by VCS.\n\n"
  },
  {
    "path": "testdata/SecurityInfo/10.11.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>CommandUUID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n        <key>RequestType</key>\n        <string>SecurityInfo</string>\n        <key>SecurityInfo</key>\n        <dict>\n            <key>FDE_Enabled</key>\n            <false/>\n        </dict>\n        <key>Status</key>\n        <string>Acknowledged</string>\n        <key>UDID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/SecurityInfo/IOS-9.x.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n\t<key>SecurityInfo</key>\n\t<dict>\n\t\t<key>HardwareEncryptionCaps</key>\n\t\t<integer>3</integer>\n\t\t<key>PasscodeCompliant</key>\n\t\t<true/>\n\t\t<key>PasscodeCompliantWithProfiles</key>\n\t\t<true/>\n\t\t<key>PasscodeLockGracePeriod</key>\n\t\t<integer>0</integer>\n\t\t<key>PasscodeLockGracePeriodEnforced</key>\n\t\t<integer>0</integer>\n\t\t<key>PasscodePresent</key>\n\t\t<false/>\n\t</dict>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>1111111111111111111111111111111111111111</string>\n</dict>\n</plist>"
  },
  {
    "path": "testdata/SecurityInfo/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>f1048316-6628-4b32-b2f2-708d7f4d7105</string>\n\t<key>SecurityInfo</key>\n\t<dict>\n\t\t<key>HardwareEncryptionCaps</key>\n\t\t<integer>3</integer>\n\t\t<key>PasscodeCompliant</key>\n\t\t<true/>\n\t\t<key>PasscodeCompliantWithProfiles</key>\n\t\t<true/>\n\t\t<key>PasscodeLockGracePeriod</key>\n\t\t<integer>0</integer>\n\t\t<key>PasscodeLockGracePeriodEnforced</key>\n\t\t<integer>0</integer>\n\t\t<key>PasscodePresent</key>\n\t\t<false/>\n\t</dict>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>1c111c111c111c111c111c111c111c111c111c11</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/SecurityInfo/macOS-10.13.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CommandUUID</key>\n\t<string>bf88d054-bd86-480d-b406-a1bc74a403c0</string>\n\t<key>SecurityInfo</key>\n\t<dict>\n\t\t<key>FDE_Enabled</key>\n\t\t<false/>\n\t\t<key>FirewallSettings</key>\n\t\t<dict>\n\t\t\t<key>Applications</key>\n\t\t\t<array>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>Allowed</key>\n\t\t\t\t\t<true/>\n\t\t\t\t\t<key>Name</key>\n\t\t\t\t\t<string>pia_openvpn</string>\n\t\t\t\t</dict>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>Allowed</key>\n\t\t\t\t\t<true/>\n\t\t\t\t\t<key>Name</key>\n\t\t\t\t\t<string>pia_openvpn_client</string>\n\t\t\t\t</dict>\n\t\t\t</array>\n\t\t\t<key>BlockAllIncoming</key>\n\t\t\t<false/>\n\t\t\t<key>FirewallEnabled</key>\n\t\t\t<false/>\n\t\t\t<key>StealthMode</key>\n\t\t\t<false/>\n\t\t</dict>\n\t\t<key>FirmwarePasswordStatus</key>\n\t\t<dict/>\n\t\t<key>SystemIntegrityProtectionEnabled</key>\n\t\t<false/>\n\t</dict>\n\t<key>Status</key>\n\t<string>Acknowledged</string>\n\t<key>UDID</key>\n\t<string>00000000-1111-2222-3333-444455556666</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/TokenUpdate/10.11.x-user.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>MessageType</key>\n        <string>TokenUpdate</string>\n        <key>NotOnConsole</key>\n        <false/>\n        <key>PushMagic</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n        <key>Token</key>\n        <data>\n            AAAA=\n        </data>\n        <key>Topic</key>\n        <string>com.apple.mgmt.test.00000000-1111-2222-3333-444455556666</string>\n        <key>UDID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n        <key>UserID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n        <key>UserLongName</key>\n        <string>Administrator</string>\n        <key>UserShortName</key>\n        <string>admin</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/TokenUpdate/10.11.x.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>AwaitingConfiguration</key>\n        <false/>\n        <key>MessageType</key>\n        <string>TokenUpdate</string>\n        <key>PushMagic</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n        <key>Token</key>\n        <data>\n            YXBwbGU=\n        </data>\n        <key>Topic</key>\n        <string>com.apple.mgmt.test.00000000-1111-2222-3333-444455556666</string>\n        <key>UDID</key>\n        <string>00000000-1111-2222-3333-444455556666</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/TokenUpdate/10.12.2-user.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n        \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>MessageType</key>\n        <string>TokenUpdate</string>\n        <key>NotOnConsole</key>\n        <false/>\n        <key>PushMagic</key>\n        <string>B81B1FEC-09C6-4EC2-871C-E521EC971B38</string>\n        <key>Token</key>\n        <data>MDAyMDEwNTItQTJEMC00MDNELUI4NTctNzJGOTEzRjVCQ0NECg==</data>\n        <key>Topic</key>\n        <string>com.apple.mgmt.commandment.dev</string>\n        <key>UDID</key>\n        <string>E3568F17-92ED-450A-8904-C3BF4CB7E9A5</string>\n        <key>UserID</key>\n        <string>A522C2FB-D0BA-487E-BBC6-BE0DB2DC7883</string>\n        <key>UserLongName</key>\n        <string>Commando Joe</string>\n        <key>UserShortName</key>\n        <string>cjoe</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/TokenUpdate/10.12.2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n        \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>AwaitingConfiguration</key>\n        <false/>\n        <key>MessageType</key>\n        <string>TokenUpdate</string>\n        <key>PushMagic</key>\n        <string>B81B1FEC-09C6-4EC2-871C-E521EC971B38</string>\n        <key>Token</key>\n        <data>MDAyMDEwNTItQTJEMC00MDNELUI4NTctNzJGOTEzRjVCQ0NFCg==</data>\n        <key>Topic</key>\n        <string>com.apple.mgmt.commandment.dev</string>\n        <key>UDID</key>\n        <string>E3568F17-92ED-450A-8904-C3BF4CB7E9A5</string>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/TokenUpdate/iOS-11.3.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>AwaitingConfiguration</key>\n        <false/>\n        <key>MessageType</key>\n        <string>TokenUpdate</string>\n        <key>PushMagic</key>\n        <string>AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA</string>\n        <key>Token</key>\n        <data>\n            Base64=\n        </data>\n        <key>Topic</key>\n        <string>com.apple.mgmt.XServer.1c111c11-1c11-1c11-1c11-1c111c111c11</string>\n        <key>UDID</key>\n        <string>1c111c111c111c111c111c111c111c111c111c11</string>\n        <key>UnlockToken</key>\n        <data>\n            base64==\n        </data>\n    </dict>\n</plist>"
  },
  {
    "path": "testdata/decrypt_dep_token.sh",
    "content": "#!/usr/bin/env bash\n\nopenssl smime -decrypt -in \"${1}\" -recip \"./dep-public.pem\"  -inkey \"./dep-key.pem\"\n"
  },
  {
    "path": "testdata/dep/profile.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>LANGUAGE</key>\n\t<string>en-AU</string>\n\t<key>PRODUCT</key>\n\t<string>iPad4,1</string>\n\t<key>SERIAL</key>\n\t<string>BLXLN1111111</string>\n\t<key>UDID</key>\n\t<string>00000000000000000000000000000000</string>\n\t<key>VERSION</key>\n\t<string>15A5278f</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "testdata/itunes/ios-search-slack.json",
    "content": "{\n  \"resultCount\": 50,\n  \"results\": [\n    {\n      \"screenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/fc/dc/5b/fcdc5bcd-2281-addb-b750-0cebfd78936f/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple62/v4/50/87/7c/50877cd2-e726-4db9-55b1-91590b39c22b/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/16/86/6f/16866fb7-7321-699d-749a-d35b9fbd61d3/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/b0/11/a3/b011a319-8fe1-1eb9-96d9-3ef93461e55f/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/5b/68/08/5b68082f-5433-d7b3-6cbd-52ee58ab01ae/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/eb/1a/20/eb1a20ee-ac93-06cb-e8ce-194ee7708007/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple62/v4/d3/9b/94/d39b9408-3b20-96f3-6f6e-66e5c915547b/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/41/35/e7/4135e704-ae40-73e2-222f-1730adf32907/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/0d/fe/54/0dfe54b5-6a73-8b5e-e64e-0f7d2de484f5/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/09/f3/72/09f3729f-2495-ee78-df00-5dffcced03ae/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/09/f3/72/09f3729f-2495-ee78-df00-5dffcced03ae/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/09/f3/72/09f3729f-2495-ee78-df00-5dffcced03ae/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/slack-technologies-inc/id453420243?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Slack\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"JA\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"166092800\",\n      \"sellerUrl\": \"https://slack.com/is\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/slack/id618783545?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2019-01-07T20:00:45Z\",\n      \"releaseNotes\": \"What’s New \\n• We now compress jpeg images while uploading them, so image uploads should be quicker and more reliable. If you're happy to sacrifice a little time for a less compressed image, that's fine too: you can toggle this setting in Settings > Advanced. \\n\\nBug Fixes \\n• Fixed: When using an external keyboard with an iPad, some keyboard shortcuts were not behaving as they should — or, in fact, at all. They have been brought back in line, and should now function just as you'd expect. \\n• Fixed: You can now use the quick switcher to switch to a DM session with someone no longer on your team. Because people may leave, but knowledge remains. It's a very useful thing that way.\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2013-03-20T19:23:34Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.59\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 453420243,\n      \"artistName\": \"Slack Technologies, Inc.\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Slack brings team communication and collaboration into one place so you can get more work done, whether you belong to a large enterprise or a small business. Check off your to-do list and move your projects forward by bringing the right people, conversations, tools, and information you need together. Slack is available on any device, so you can find and access your team and your work, whether you’re at your desk or on the go.\\n\\nUse Slack to: \\n• Communicate with your team and organize your conversations by topics, projects, or anything else that matters to your work\\n• Message or call any person or group within your team\\n• Share and edit documents and collaborate with the right people all in Slack \\n• Integrate into your workflow, the tools and services you already use including Google Drive, Salesforce, Dropbox, Asana, Twitter, Zendesk, and more \\n• Easily search a central knowledge base that automatically indexes and archives your team’s past conversations and files\\n• Customize your notifications so you stay focused on what matters\\n\\nScientifically proven (or at least rumored) to make your working life simpler, more pleasant, and more productive. We hope you’ll give Slack a try.\\n\\nHaving trouble? Please reach out to feedback@slack.com\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.tinyspeck.chatlyio\",\n      \"trackName\": \"Slack\",\n      \"trackId\": 618783545,\n      \"sellerName\": \"Slack Technologies, Inc.\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 675\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/de/95/24/de9524b2-4d1e-74b3-b911-dadf61c4251b/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/4b/dd/1c/4bdd1c11-1782-6bfd-dbe3-397b22b7b744/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/61/83/4b/61834be6-cb58-6601-10d4-3e3c10442574/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/c2/91/2a/c2912a7d-bbb4-fd90-fde1-f0e3cc8cb0bf/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/34/3f/98/343f9860-1a1b-32b0-a465-9ab658854ff5/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/c9/6c/c8/c96cc830-d281-a1e0-cbef-54fdf887283e/source/552x414bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/c7/14/a5/c714a5fe-239f-7040-df6a-6b376b798e01/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/01/b2/55/01b255fa-4951-148c-e107-6989ed54c6d2/source/552x414bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/92/57/7d/92577d3a-a757-875a-d2aa-0daebce717ce/source/552x414bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/f6/0d/aa/f60daa14-8310-9714-d10a-4ae13638a727/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/7a/ce/74/7ace741f-f199-f0e8-bea9-87caf1864b7f/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/7a/ce/74/7ace741f-f199-f0e8-bea9-87caf1864b7f/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/7a/ce/74/7ace741f-f199-f0e8-bea9-87caf1864b7f/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/microsoft-corporation/id298856275?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 4.5,\n      \"trackCensoredName\": \"Microsoft Teams\",\n      \"languageCodesISO2A\": [\n        \"AK\",\n        \"AR\",\n        \"BG\",\n        \"CA\",\n        \"HR\",\n        \"CS\",\n        \"DA\",\n        \"NL\",\n        \"EN\",\n        \"ET\",\n        \"FI\",\n        \"FR\",\n        \"DE\",\n        \"EL\",\n        \"HE\",\n        \"HU\",\n        \"IS\",\n        \"ID\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"LV\",\n        \"LT\",\n        \"NB\",\n        \"NN\",\n        \"PL\",\n        \"PT\",\n        \"RO\",\n        \"RU\",\n        \"SR\",\n        \"ZH\",\n        \"SK\",\n        \"SL\",\n        \"ES\",\n        \"SV\",\n        \"TH\",\n        \"ZH\",\n        \"TR\",\n        \"UK\",\n        \"VI\",\n        \"CY\"\n      ],\n      \"fileSizeBytes\": \"143855616\",\n      \"sellerUrl\": \"http://aka.ms/microsoftteams\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 139,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/microsoft-teams/id1113153706?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-22T00:34:34Z\",\n      \"releaseNotes\": \"Bug fixes and performance improvements\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2016-11-02T21:19:53Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.0.61\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 298856275,\n      \"artistName\": \"Microsoft Corporation\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Microsoft Teams is your hub for teamwork in Office 365. All your team conversations, files, meetings, and apps live together in a single shared workspace, and you can take it with you on your favorite mobile device. Whether you’re sprinting towards a deadline or sharing your next big idea, Teams can help you achieve more.\\n\\nYOUR HUB FOR TEAMWORK\\n* Easily manage your team’s projects with file editing and sharing on the go\\n* Connect face-to-face with HD audio and video, and join meetings from almost anywhere\\n* Chat privately or in groups, and communicate with the entire team in dedicated channels\\n* Mention individual team members, or the whole team at once, to get your colleagues’ attention\\n* Focus on what matters most by saving important conversations and customizing your notifications\\n* Search your chats and team conversations to quickly find what you need\\n* Get the enterprise-level security and compliance you expect from Office 365\\n\\nThis app requires a paid Office 365 commercial subscription, or a free or trial subscription of Microsoft Teams. If you’re not sure about your company’s subscription or the services you have access to, visit Office.com/Teams to learn more or contact your IT department.\\n\\nBy downloading Teams, you agree to the license (see aka.ms/eulateamsmobile) and privacy terms (see aka.ms/privacy). For support or feedback, email us at mtiosapp@microsoft.com\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.microsoft.skype.teams\",\n      \"trackName\": \"Microsoft Teams\",\n      \"trackId\": 1113153706,\n      \"sellerName\": \"Microsoft Corporation\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 6766\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/1c/24/33/1c2433b9-b675-7e02-5485-48920342507d/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/26/ef/f0/26eff0e3-0447-68e1-4642-49c237368109/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/f6/11/a1/f611a1f0-a919-9d7c-4fa6-ca0e05482176/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/29/33/33/2933339a-eebd-7f60-114d-ac8bbf01008b/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/52/3b/7d/523b7d79-78e9-1af2-3ec7-448d09226b61/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/ff/af/e0/ffafe074-89e5-c167-dc66-3416688a0ad5/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/45/f1/cb/45f1cb0b-982f-cec1-e26b-afb2bd47fdab/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/56/c8/05/56c805b8-2240-7fc2-fcb4-4449a08da981/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/00/dc/5e/00dc5e8e-05bf-27eb-6e67-668211fd49ce/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/d0/35/55/d0355525-7805-3123-547a-bd1f25d8dfc0/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/9b/05/ac/9b05ac49-510b-e7cf-b278-0ae9b8ebcd2c/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/07/1b/b0/071bb00e-8d91-702a-bf79-9537455a80e7/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/42/3d/fb/423dfb2e-dd8d-f9cd-f3ef-2a2cdc050460/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/42/3d/fb/423dfb2e-dd8d-f9cd-f3ef-2a2cdc050460/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/42/3d/fb/423dfb2e-dd8d-f9cd-f3ef-2a2cdc050460/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/ifttt/id660944638?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 4.5,\n      \"trackCensoredName\": \"IFTTT\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"71264256\",\n      \"sellerUrl\": \"https://ifttt.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 205,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/ifttt/id660944635?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-10T21:32:38Z\",\n      \"releaseNotes\": \"+ We fixed an issue that caused your Activity feed and My Applets to slow down.\\n\\n+ Recently launched services on IFTTT include: Ai-Sync, Fanimation, Aquanta, Home + Control, and Mitsubishi Electric kumo cloud.\\n\\n+ The latest version of the IFTTT app includes a security improvement for iOS Photos Applets and the Camera widget.\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2013-07-11T07:00:00Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.7.8\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 660944638,\n      \"artistName\": \"IFTTT\",\n      \"genres\": [\n        \"Productivity\",\n        \"Utilities\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Applets bring your favorite services together to create new experiences.\\n\\nOver 600 apps work with IFTTT including Twitter, Telegram, Google Drive, Twitch, Weather Underground, Instagram, Gmail, and devices like Google Home, Amazon Alexa, Nest, Philips Hue, and your iPhone. The IFTTT app also integrates with the Health app, so you can easily track and maintain your habits. \\n\\nTurn on Applets and:\\n\\n• Control everything around you with your voice and Amazon Alexa or Google Assistant\\n• Stay informed about what’s happening from publications like The New York Times and ProPublica\\n• Always stay prepared for the weather with custom daily forecast notifications\\n• Message roommates when you’re near the local grocery\\n• Get an alert as soon as there’s a new Craigslist listing that matches you search\\n• Stay safe with automated and intelligent home security alerts\\n• Streamline your social media\\n• Back up and share your iOS photos automatically\\n• Back up important files, photos, and contacts to cloud-storage solutions, such as Dropbox or Google Drive\\n• Set your home thermostat to an optimal temperature when you arrive home\\n• Post all your Instagrams as Twitter photos or Pinterest pins\\n• Trigger events based on your current location\\n\\n\\nThere are thousands of other use cases! New services are added every week. Some popular ones include:\\n\\nTwitch, Telegram, Spotify, YouTube, Google Calendar, Tumblr, Medium, Pocket, Square, eBay, Giphy, Automatic, LIFX, Fitbit, Withings, littleBits, Google WiFi, Evernote, Reddit, Digg, Skype, Slack, LINE, MailChimp, Salesforce, Todoist, and hundreds more.\\n\\nBrowse our curated collections to find Applets for:\\n\\n• The home, office, and car\\n• Staying informed on news and politics\\n• Your iPhone and iPad\\n• Exploring outer space\\n• Improving how you use social media\\n\\nDo more with the services you love. Discover the power of Applets at ifttt.com/discover\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.ifttt.ifttt\",\n      \"trackName\": \"IFTTT\",\n      \"trackId\": 660944635,\n      \"sellerName\": \"IFTTT Inc\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 1924\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/10/03/cd/1003cd0d-c7b7-e101-6517-f073898cee64/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple69/v4/94/0c/6a/940c6ad6-8292-7659-548a-c04627f16538/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple69/v4/64/be/70/64be7070-633e-456c-c4a0-34c6f84993c9/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple69/v4/7c/9c/b1/7c9cb17d-5984-b75e-a722-d52422ce09ad/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple49/v4/35/29/4a/35294a32-6456-1ac0-6faf-4da93b826e5e/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple69/v4/69/71/ae/6971aeff-e935-4bc9-f0ef-d6cc899effb8/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/5f/58/f6/5f58f688-d14b-2350-7e8c-197d5e00d82b/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple69/v4/2c/9e/2a/2c9e2a6e-2c83-6c8d-48c8-8185476b7c78/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple69/v4/fe/86/b1/fe86b171-52b6-c909-942e-d78d36cbce33/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple49/v4/f4/7c/9b/f47c9b96-ec55-f3b3-b0cb-28e7eee919d5/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple127/v4/c5/75/d1/c575d1b5-6f1d-0e50-6c1e-62524fd51b3a/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple127/v4/c5/75/d1/c575d1b5-6f1d-0e50-6c1e-62524fd51b3a/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple127/v4/c5/75/d1/c575d1b5-6f1d-0e50-6c1e-62524fd51b3a/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/weipei-deng/id1042972016?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 4.5,\n      \"trackCensoredName\": \"CountDown Tracker to Christmas Birthday Date Event\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"RU\",\n        \"ZH\",\n        \"ES\",\n        \"ZH\"\n      ],\n      \"fileSizeBytes\": \"62970880\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 14,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/countdown-tracker-to-christmas-birthday-date-event/id647750636?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-05-31T08:33:06Z\",\n      \"releaseNotes\": \"* Bug fixes and stability improvements\",\n      \"primaryGenreId\": 6002,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6002\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2013-05-21T03:25:40Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.5\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1042972016,\n      \"artistName\": \"WEIPEI DENG\",\n      \"genres\": [\n        \"Utilities\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Sometimes our life is frittered away by detail, making us forget something important. Hence, a smart Countdown app to remind us is extremely necessary.\\n\\nCount the future and past dates as well. Just add events that you’ve been expecting: anniversary, birthday, Valentine's Day, Halloween, Christmas, etc. Also, you can count the past events such as: the first date with your darling, the date you were born…\\n\\nIt can be shown on the Notification Widget conveniently, and the timer would ring to remind you when time runs out. From now on, catch every precious moment, to “let your life lightly dance on the edges of Time, like dew on the tip of a leaf.”\\n\\nCool Features:\\n- Show days, hours, minutes and even seconds\\n- Displayed on Notification Widget conveniently\\n-Smart Reminder are provided\\n-Various wallpapers for any occasion\\n-Customize event backgrounds \\n-Share your happiness with others\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Utilities\",\n      \"bundleId\": \"flyman.countdownlite\",\n      \"trackName\": \"CountDown Tracker to Christmas Birthday Date Event\",\n      \"trackId\": 647750636,\n      \"sellerName\": \"WEIPEI DENG\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 614\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/de/79/12/de7912b3-aa08-6802-8b0f-3672fc3a117c/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/97/d2/58/97d25815-cc8d-d2f7-08d7-a210e612a7f3/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/eb/80/a5/eb80a516-dbd4-5bcc-2682-2166d99e604f/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/99/f2/b1/99f2b14f-f8cb-4301-6901-446b0b769366/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/98/51/59/9851599b-9b63-d1f7-d27d-b51872fa0bc9/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/ac/ff/d3/acffd339-effe-4af0-2d6b-e463c4a9d381/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/4a/6c/38/4a6c3856-8eee-0fe1-7f52-b334b7a236d6/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/e2/8d/44/e28d4496-8863-dab9-7056-8f63dfdd62d0/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/a5/44/59/a54459eb-b189-caca-e7f5-edf3a88abcdc/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/8b/3a/3d/8b3a3db4-7328-9a2c-0280-166c1c67f322/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/8b/3a/3d/8b3a3db4-7328-9a2c-0280-166c1c67f322/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/8b/3a/3d/8b3a3db4-7328-9a2c-0280-166c1c67f322/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/slack-technologies-inc/id453420243?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"Slack for EMM\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"JA\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"162906112\",\n      \"sellerUrl\": \"https://slack.com/is\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 1,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/slack-for-emm/id1254292716?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-13T21:51:49Z\",\n      \"releaseNotes\": \"Bug Fixes \\n• Fixed: If you have a DM conversation containing only one message, you can, once more, long press to mark that message as unread, and come back to it later. Or not. Up to you. \\n• Fixed: Changing status was proving inconceivably tricky for people whose workspace had customized the list of statuses. Now you can select, deselect, reselect, and customize your status as you wish. \\n• Everything else is fine. (We hope.)\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2017-08-07T20:54:16Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.58\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 453420243,\n      \"artistName\": \"Slack Technologies, Inc.\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Slack for EMM is for Slack customers with Enterprise Mobility Management (EMM) enabled. \\nIf you’re unsure whether this applies to your organization, we recommend using the regular Slack app.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.slack.slackmdm\",\n      \"trackName\": \"Slack for EMM\",\n      \"trackId\": 1254292716,\n      \"sellerName\": \"Slack Technologies, Inc.\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/c1/18/82/c11882cb-e0fb-b151-83c0-92f4dd43a76b/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/4f/d7/a0/4fd7a04f-5355-feb7-1587-17db4f44d5fb/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/40/1d/08/401d0879-309f-610a-e120-4dc5b0fe21a4/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/9e/c1/d6/9ec1d6b6-53a7-3951-937d-d9d5edf66b6b/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/cc/a2/81/cca28173-81d4-84d1-4c2b-8d6fef9df8ed/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/4c/0d/7c/4c0d7c14-d51b-8bf0-2120-832edcc20112/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/b5/61/b0/b561b044-d429-fc51-11f0-b868f811d858/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/00/3a/7e/003a7e1f-4bd2-6609-cecb-d71dcf6f8c88/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/a8/31/b8/a831b8cb-4fd4-c731-8750-f10ab78b74d5/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/26/b0/91/26b09105-e141-fb49-c0fd-2fd0165055c6/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/bd/1c/53/bd1c53f8-9aae-f2f3-909c-77c67d95ad08/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/1a/b5/41/1ab541cc-03b6-a226-f003-6112994996c1/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/c7/0b/d2/c70bd23b-4524-4e09-7ef6-2f2f725a9585/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/a4/5b/29/a45b293a-db19-f831-df5e-21fa0c44b240/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/6b/cf/51/6bcf51be-1a35-e032-07ac-38f73607bc1c/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/4b/ef/f6/4beff6d0-a6e3-782b-2ec8-92fdb16d6a97/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/32/b3/6f/32b36fa8-054c-d6e0-d3bc-ae5cf566231f/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/5c/93/9e/5c939ee1-589b-448a-04b3-22c20adfaf29/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/ee/fb/23/eefb23c3-6e5a-23ec-6db1-92ab6ec42408/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/e2/c5/00/e2c5007f-d80e-e4ee-66f0-9d0b02f176c2/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/f4/6a/61/f46a61bf-49fa-1e4f-35fe-1a005ed40ab1/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/f4/6a/61/f46a61bf-49fa-1e4f-35fe-1a005ed40ab1/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/f4/6a/61/f46a61bf-49fa-1e4f-35fe-1a005ed40ab1/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/readdle-inc/id285035419?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 4.5,\n      \"trackCensoredName\": \"Email - Spark by Readdle\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"JA\",\n        \"PT\",\n        \"RU\",\n        \"ZH\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"132473856\",\n      \"sellerUrl\": \"http://sparkmailapp.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 111,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/email-spark-by-readdle/id997102246?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-12T09:10:45Z\",\n      \"releaseNotes\": \"Today's update brings you a handful of important fixes and performance improvements.\\nLet's dive into the release note: \\n## Fixed\\n- Odd scenario when snoozed emails due to unstable Internet connection returned to your Inbox earlier than they should. This should no longer happen, we promise.\\n## Improved\\n- Contact suggestions in a composer. Thank you to everyone for your feedback on contact duplicates, naming issues and irrelevant suggestions in the email composer. We invested a ton of time in identifying and improving those. Suggestions will now also prioritize real people you contacted over automated services.\\n\\nWe hope you love the update as much as we do! \\nKeep the feedback coming at rdsupport@readdle.com and stay tuned for news and exciting features.\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2015-05-29T11:07:57Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"2.1.4\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 285035419,\n      \"artistName\": \"Readdle Inc.\",\n      \"genres\": [\n        \"Productivity\",\n        \"Utilities\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Spark is the best personal email client and a revolutionary email for teams. You will love your email again! \\n\\n\\\"Best of the App Store\\\" - Apple\\n\\\"It's a combination of polish, simplicity, and depth\\\" - FastCompany\\n\\\"You can create an email experience that works for you\\\" - TechCrunch\\n\\n**Beautiful and Intelligent Email App**\\nWe are building the future of email. Modern design, fast, intuitive, collaborative, seeing what’s important, automation and truly personal experience that you love - this is what Spark stands for.\\n\\n**Farewell to Busy Inbox**\\nSmart Inbox lets you quickly see what's important in your inbox and clean up the rest. All new emails are smartly categorized into Personal, Notifications and Newsletters.\\n\\n**Discuss email privately**\\nInvite teammates to discuss specific emails and threads. Ask questions, get answers, and keep everyone in the loop.\\n\\n**Create email together**\\nFor the first time ever, collaborate with your teammates using real-time editor to compose professional emails.\\n\\n**Schedule emails to be sent later**\\nSchedule emails to be sent when your recipient is most likely to read them. It works even if your device is turned off.\\n\\n**Snooze That One For Later**\\nSnooze an email and get back to it when the time is right. Snoozing works across all your Apple devices.\\n\\n**Find Any Email In An Instant**\\nPowerful, natural language search makes it easy to find that email you're looking for. Just search the way you think and let Spark do the rest.\\n\\n**Get Notified About Important Emails Only**\\nSmart Notifications filter out the noise, letting you know when an email is important, saving you from notification overload.\\n\\n**Powerful Integrations**\\nIntegrate Spark into your workflow and take productivity to the next level. Supports Dropbox, Box, iCloud Drive, and more.\\n\\n**Built-in calendar**\\nA full-featured calendar works right in your email to help you always be on top of your schedule. Create events easily using natural language.\\n\\n**Create links to email**\\nCreate secure links to a specific email or conversation. Share the link on Slack, Skype, CRM, or any other medium so your team can see it and collaborate around it.\\n\\n**Sign Off With A Swipe**\\nBefore you send an email, quickly swipe to choose the right signature for the occasion.\\n\\n**Email with Emotion**\\nQuick Replies get the point across with just a tap. Love, like or acknowledge an email in an instant.\\n\\n**Email Never Looked This Good**\\nThat terrible mess in your inbox is now replaced it with a beautiful, threaded message design.\\n\\n**A Truly Personal Experience**\\nCustomize Spark to work as you do. You decide which swipes do what, what cards are shown, and how many emails you want to see.\\nYou’ll love your email again!\\n \\nIf you need us, you can always find us at rdsupport@readdle.com\",\n      \"minimumOsVersion\": \"11.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.readdle.smartemail\",\n      \"trackName\": \"Email - Spark by Readdle\",\n      \"trackId\": 997102246,\n      \"sellerName\": \"Readdle Inc.\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 2993\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple30/v4/04/48/53/044853ef-ab01-5f7d-a048-7326776e9179/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple60/v4/06/ab/f3/06abf357-30b7-d3f4-c5c0-5762582aa638/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple20/v4/69/ef/f9/69eff9c3-3368-9c16-c9cf-fcf9d45e9806/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple18/v4/85/05/bb/8505bba4-bc30-2cbe-1a3e-ddda4ea2be9c/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple20/v4/10/ab/f6/10abf68d-f37a-e32e-2178-428e5c588b4a/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/e1/12/ba/e112ba92-f938-b351-060f-470d81d87b0a/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/e1/12/ba/e112ba92-f938-b351-060f-470d81d87b0a/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/e1/12/ba/e112ba92-f938-b351-060f-470d81d87b0a/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/pablo-episcopo/id1081953530?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"averageUserRatingForCurrentVersion\": 3.5,\n      \"trackCensoredName\": \"Recordify - Quickly send audio messages to Slack\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"9746432\",\n      \"sellerUrl\": \"https://www.recordify.io\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 3,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/recordify-quickly-send-audio-messages-to-slack/id1081953531?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2016-05-09T13:32:44Z\",\n      \"releaseNotes\": \"As easy and fast as always. But better.\\n\\n• With multi-account feature you can login to all your teams.\\n• Send voice to Private Groups and user through Direct Message. \\n• Rating Reminder integrated on this version. We’re not gonna spam you, promise!\\n• A few small bug fixes and performance Improvements.\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2016-03-14T12:14:06Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.1\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1081953530,\n      \"artistName\": \"Pablo Episcopo\",\n      \"genres\": [\n        \"Productivity\",\n        \"Utilities\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Give Slack superpowers! Send voice messages instantly.\\n\\nRecordify is a refined, well-crafted, decidedly straightforward single feature app. Boost your productivity and experience how minimalist visuals hide simplicity reimagined.\\n\\nThree simple rules:\\n\\n• Hold to Record.\\n• Drag to Cancel.\\n• Release to Send.\\n\\nWe love feedback! So email us at feedback@recordify.io\\n\\nFollow us on our Twitter @recordifyio\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.recordifyio\",\n      \"trackName\": \"Recordify - Quickly send audio messages to Slack\",\n      \"trackId\": 1081953531,\n      \"sellerName\": \"Pablo Episcopo\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple62/v4/a6/d7/ce/a6d7cecd-1ff4-0936-4015-fc3011cc1fe7/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple41/v4/1d/e6/41/1de6416a-c1d1-6c92-c5f7-aa6c6d774324/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple71/v4/ec/c4/67/ecc46760-b60b-f7cc-4da0-ecd35374a7cd/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/40/0e/5b/400e5b6e-b6e3-fbec-defb-9ed6c43a953b/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple62/v4/ec/f7/77/ecf777e6-6053-5a8e-0ec0-9b29bfb78550/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/d5/57/61/d5576193-9c94-0b87-2406-df8a737cd819/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/d5/57/61/d5576193-9c94-0b87-2406-df8a737cd819/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/d5/57/61/d5576193-9c94-0b87-2406-df8a737cd819/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/companyons/id661578187?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"averageUserRatingForCurrentVersion\": 3.5,\n      \"trackCensoredName\": \"Kyber for Slack | Project Management, Todo & Task\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"PT\",\n        \"RU\",\n        \"ES\",\n        \"TR\"\n      ],\n      \"fileSizeBytes\": \"38853632\",\n      \"sellerUrl\": \"http://kyber.me\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 2,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/kyber-for-slack-project-management-todo-task/id898004872?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-01-19T22:10:36Z\",\n      \"releaseNotes\": \"Calendar sync fix.\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2014-07-25T01:38:27Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.2.19\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 661578187,\n      \"artistName\": \"Companyons\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Make things happen with the people you care about! For teams, families, couples, friends. And for Slack: http://kyber.me/slack\\n\\nFeatured by Apple as \\\"Best New App\\\", Kyber is a fantastic new app that integrates messaging with your calendars, reminders, to-dos, maps to get more done together and make your life so much easier. Kyber also lets you keep everything you have going on in your personal or work life under control: finally one single place to view your daily  calendars, reminders, and todos combined together.\\n\\nKyber can be added to your Slack team to turn messages into actions or integrated with IFTTT to extend it to any other apps like Gmail, Evernote, etc. Learn more at http://kyber.me/slack and http://kyber.me/IFTTT\\n\\nExciting way to use Kyber\\n====================\\nUse Kyber to:\\n• keep your family organized with shared events and reminders for daily chores\\n• make your team or business highly efficient with tasks easily assigned to each other and meetings instantly set up\\n• take the pain out of organizing fun activities with your friends with smart messaging\\n• manage your day with all your personal and shared tasks in one place.\\n\\nKyber is the simplest to use: just type what to do as you would speak it and add few selected emojis to magically:\\n• send reminders to others going off at specified time\\n• check and update calendars while typing an invite\\n• search for a place or address and add a map to your message\\n• add a checklist to track items (grocery shopping anyone?)\\n• ask somebody to do something for you and track it until is done\\n• easily organize something with others\\n• create personal tasks with time, location, checklist, notes\\n\\nSlack and IFTTT\\n============\\nYou can use Kyber along with Slack to access your calendars, reminders, to-dos from your desktop and easily assign tasks to co-workers or schedule meetings with natural language. Kyber is also powered by IFTTT to integrate your workflows with hundreds of apps such as Gmail, Evernote, Weather and much more.\\nLearn more at http://kyber.me/slack and http://kyber.me/ifttt.\\n\\nFew examples\\n===========\\nImagine to send a message like “Let’s meet for coffee at 5:30 PM at Starbucks” and...\\n• Know in advance if you or the other person are free at 5:30 PM to minimize the “Are you free?” back and forth\\n• Automatically add the event to each other calendars and later get reminded about it\\n• Allow the other person to update the event just replying “Let’s do 4:30 PM” or “I rather do Peet’s Coffee\\\"\\n• Check maps and direction with a tap when it’s time to leave\\n• Chat in the context of the specific event\\n\\nOr another message that says: “Can you pick up grocery 5 PM  Whole Foods? bread, milk, eggs” and...\\n• Add it to recipient to-do list so it can be tracked (and get done)\\n• Have an alert going off at 5 PM to remind the recipient\\n• Add a checklist that can be edited by both and then checked off while shopping\\n• Message while going through the list\\n• Automatically get notified when the task is done\\n• Or... easily follow up if not done yet\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.companyons.loop\",\n      \"trackName\": \"Kyber for Slack | Project Management, Todo & Task\",\n      \"trackId\": 898004872,\n      \"sellerName\": \"Companyons, Inc.\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple1/v4/9d/ba/f7/9dbaf7fa-3401-3533-685e-18d0de5e2042/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/e9/66/29/e96629ea-b02a-6497-b47e-4d72270f6f90/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple5/v4/4e/b2/9b/4eb29bef-f7da-c028-c989-aec7cf0dd06f/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/c0/dc/14/c0dc1420-48fd-ec99-a850-7952b41c15fc/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple1/v4/69/fe/04/69fe0423-5670-8b98-7cef-1524a8695b30/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/1d/36/b1/1d36b1d8-9dda-e89c-9d0b-3b0edb317959/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/1d/36/b1/1d36b1d8-9dda-e89c-9d0b-3b0edb317959/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/1d/36/b1/1d36b1d8-9dda-e89c-9d0b-3b0edb317959/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/hooloop-corporation/id889892100?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone4-iPhone4\",\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"averageUserRatingForCurrentVersion\": 1.0,\n      \"trackCensoredName\": \"Hooloop Memo - Voice for Slack\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"9134080\",\n      \"sellerUrl\": \"https://www.hooloop.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 1,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/hooloop-memo-voice-for-slack/id967109730?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2015-08-06T01:26:57Z\",\n      \"releaseNotes\": \"Don't show deleted members in the members list anymore.\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2015-04-16T17:46:35Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.3.2\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 889892100,\n      \"artistName\": \"Hooloop Corporation\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Slack is a fantastic platform for teams. And if you are part of a team that holds daily standup/scrum meetings, you've heard this one before \\\"let's take the questions and comments offline!\\\" With Hooloop Memo you can easily record your daily standup as voice messages, automatically post them to Slack, and keep the conversation going. \\n\\nEver wanted to leave a quick message to your Slack team without typing? Now you can! Record a voice message and send it to any of your Slack channels.\\n\\nHooloop Memo is based on the Hooloop voice communication service. Hooloop voice messages can be accessed on the mobile Slack app, the Slack app for Mac, and the Slack web app. \\n\\n______◢◤ FEATURES ◥◣______\\n\\n▪ Easy, simple and fast\\n▪ Sign in using your Slack accounts\\n▪ Chose from any of your channels, private groups or team members\\n▪ Messages are posted to Slack within seconds\\n\\nPlease note that Hooloop Memo is not affiliated, associated, endorsed by, or in any way officially connected with Slack.\",\n      \"minimumOsVersion\": \"7.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.hooloop.dementor\",\n      \"trackName\": \"Hooloop Memo - Voice for Slack\",\n      \"trackId\": 967109730,\n      \"sellerName\": \"Hooloop Corporation\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/24/b9/7a/24b97a13-262c-95b7-f8bb-771306e54efd/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/ae/8b/7f/ae8b7fd4-6385-b92d-2652-5affd858ac2b/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/bf/99/53/bf99534e-042c-6edd-f264-0e65b80b3027/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/87/38/87/873887d3-86da-575e-8bcc-33e9546e0a93/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/60/d1/b0/60d1b054-e110-5604-3aa9-ef43ebc9a333/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/58/f2/1d/58f21d26-2a43-e827-3daf-dac98fd639b7/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/88/ff/c2/88ffc238-3834-03c6-8525-6b688b71b134/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/88/ff/c2/88ffc238-3834-03c6-8525-6b688b71b134/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/88/ff/c2/88ffc238-3834-03c6-8525-6b688b71b134/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/constflash/id883373065?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"Widget for Slack Lite\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"5275648\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 1,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/widget-for-slack-lite/id1183240030?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-10-31T00:34:10Z\",\n      \"releaseNotes\": \"bug fixed\\nquick answers\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6000\"\n      ],\n      \"releaseDate\": \"2016-12-08T06:11:17Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.5\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 883373065,\n      \"artistName\": \"ConstFlash\",\n      \"genres\": [\n        \"Productivity\",\n        \"Business\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Now you can access all important information about your Slack's teams from your lock screen or any app. Add Widgets for Slack to the Notification Center - and you will get quick access to Slack's functions.\\n\\nWith this app you can see all unread messages, channels and people. \\nEasy navigation through sections will provide you quick access to helpful information.\\n\\nYou can jump directly from this widget to any channel of Slack's official app.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.constflash.widgetslackfree\",\n      \"trackName\": \"Widget for Slack Lite\",\n      \"trackId\": 1183240030,\n      \"sellerName\": \"ConstFlash LTD\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/be/9d/90/be9d9061-df65-7f41-8156-fa7912b0ce82/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/cf/df/4d/cfdf4d7d-1f28-7c0f-81a1-7a4a3f66744a/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/2c/d2/84/2cd2843d-c656-2a6a-cc7d-e8dcf73f880d/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/2a/cb/2c/2acb2ced-8099-4d93-8bdd-757afc52d64f/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/11/fb/9b/11fb9b58-9144-c138-b782-60dca84ce1a1/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/8e/68/f9/8e68f980-5bc9-3209-a5d2-d013e0e99d2d/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/8e/68/f9/8e68f980-5bc9-3209-a5d2-d013e0e99d2d/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/8e/68/f9/8e68f980-5bc9-3209-a5d2-d013e0e99d2d/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/tmall-com/id518966504?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"千牛–卖家移动工作台\",\n      \"languageCodesISO2A\": [\n        \"ZH\",\n        \"EN\",\n        \"ZH\",\n        \"ZH\"\n      ],\n      \"fileSizeBytes\": \"227956736\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/%E5%8D%83%E7%89%9B-%E5%8D%96%E5%AE%B6%E7%A7%BB%E5%8A%A8%E5%B7%A5%E4%BD%9C%E5%8F%B0/id590217303?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-17T09:50:41Z\",\n      \"releaseNotes\": \"We update the app regularly so we can make it better for you:\\n[New Features] Alibaba workbench rights center, more choices for you\\n[Optimization] Optimize the performance and user experiences\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2013-01-24T18:12:35Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"7.0.50\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 518966504,\n      \"artistName\": \"Tmall.com\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"----With Alibaba Workbench, you can handle anything----\\n\\n√ A mobile seller workbench that is an official product of Alibaba\\n\\n√ Provides services for businesses such as Alibaba.com ，Taobao, 1688, offline stores, , etc., and is an essential item for any business\\n\\n√ Dedicated to providing solutions for businesses and increasing operational efficiency.\\n\\n----Basic Description----\\n\\nAlibaba Workbench is an official product of Alibaba that serves as a dedicated e-commerce store operation, business management and information mobile tool for sellers from China and all over the world. Through Alibaba Workbench, you can easily manage store merchandise and orders, check shop data and messages, process quotes, and take hold of business opportunities anytime, anywhere, allowing you to best manage your scattered time and better interact with potential buyers.\\n\\n----Main Features----\\n\\n[Workbench] Provides core operational data and a personalized operation page that includes plugins for things like products, order transactions, member management, inquiries and requests for quotation (RFQ). Shop owners can add or delete plugins freely to build their own personalized workbench.\\n\\n[Messages] Notification messages for the first time receiving goods, orders, business opportunities and campaigns.\\n\\n[Chat] Inquiries from buyers can be answered in the blink of an eye. Alibaba Workbench supports a computer and mobile device being logged into the same account at the same time so that no transactions will be missed. There are also several ways to chat such as voice and video chat, and you can also check the read/unread status of messages and quickly understand what the buyer wants.\\n\\n[Services] Provides e-commerce services to businesses and increases their operational efficiency.\\n\\n[Headlines] Abundant and up-to-date e-commerce news with diverse content such as official laws, marketing strategies, high-quality products and live videos.\\n\\n \\n\\n----Contact Us----\\n\\nIf you encounter any problems while using the workbench, please contact us at:\\n\\n-       Online assistance: Go to Alibaba Workbench > My > Settings > Questions and Feedback\\n\\n-       WANGWANG Community: Join the WANGWANG Community at 841497597, the password is qianniu1234\\n\\n-       Alibaba.com International Business Contact Telephone: (+86) 400-826-1688\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.taobao.sellerplatform\",\n      \"trackName\": \"千牛–卖家移动工作台\",\n      \"trackId\": 590217303,\n      \"sellerName\": \"Zhejiang Taobao Mall Technology Co,Ltd.\",\n      \"averageUserRating\": 2.0,\n      \"userRatingCount\": 13\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/e0/28/bf/e028bf1b-4cea-6e62-7bf7-fed9f69c1d71/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/27/6f/84/276f84bf-a6b7-c22d-715c-0a0f99882dec/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/3f/23/93/3f2393a8-d54a-3605-3821-1b718b86c629/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/a5/80/fa/a580fab5-1292-2234-e133-d454cfcec2dd/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/a5/80/fa/a580fab5-1292-2234-e133-d454cfcec2dd/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/a5/80/fa/a580fab5-1292-2234-e133-d454cfcec2dd/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/yusuke-murata/id704570686?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"MultiTeam for Slack - Multiple Team Communitation\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"JA\"\n      ],\n      \"fileSizeBytes\": \"53404672\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/multiteam-for-slack-multiple-team-communitation/id1091407464?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-07-18T00:08:18Z\",\n      \"releaseNotes\": \"- Show unread count to icon badge\\n- Fix channel order\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6005\"\n      ],\n      \"releaseDate\": \"2016-04-14T19:11:46Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.1.8\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 704570686,\n      \"artistName\": \"Yusuke Murata\",\n      \"genres\": [\n        \"Productivity\",\n        \"Social Networking\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"MultiTeam is a Slack (team communication service) client for multiple team users.\\nYou can easily switch channels across different teams.\\n\\n- Sign in to up to 3 teams for free, and unlimited teams after in-app purchase. \\n- Sort channels and direct messages in all teams by latest message timestamp as standard messenger apps.\\n- Send \\\"like\\\" emoji with one tap!\\n\\nIf you have any feature requests or bug reports, please feel free to contact @MultiTeamSlack via twitter in English or Japanese.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.muratayusuke.slackmsg\",\n      \"trackName\": \"MultiTeam for Slack - Multiple Team Communitation\",\n      \"trackId\": 1091407464,\n      \"sellerName\": \"Yusuke Murata\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/d7/35/57/d7355752-3882-0a20-a713-b4354365a5e0/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/ad/53/ad/ad53ad21-bc9e-d158-f4b9-f7e5228d15c0/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/e4/bc/c7/e4bcc7f1-5b36-c48d-8e22-3f9e0aa29d3c/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/86/5c/75/865c75d7-a997-1e10-8a32-f131cf8ee075/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/e2/14/56/e2145655-1266-ac98-8cee-8ab104dc0dcc/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/d4/d5/4a/d4d54a3b-4c07-a331-2bc6-e66a5c73f824/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/ca/28/b4/ca28b442-9eaa-fb8e-566a-f90f7e9ab92a/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/23/97/87/239787ec-bb33-dca4-6b8e-08262470faae/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/e8/ef/57/e8ef5715-db7c-4553-0cb2-3e2adc6dd044/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/bf/b9/a5/bfb9a5d4-86e6-d155-87cd-1f53e1b79f14/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/7e/74/b7/7e74b70d-a7d2-f641-f6bd-1ea5e2a5a20b/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/7f/a0/e3/7fa0e342-d001-fb6c-0458-022a655c1666/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/7f/a0/e3/7fa0e342-d001-fb6c-0458-022a655c1666/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/7f/a0/e3/7fa0e342-d001-fb6c-0458-022a655c1666/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/bloop-s-r-l/id389546852?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Airmail - Your Mail With You\",\n      \"languageCodesISO2A\": [\n        \"AR\",\n        \"CA\",\n        \"HR\",\n        \"CS\",\n        \"DA\",\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"HE\",\n        \"HU\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"NB\",\n        \"NN\",\n        \"PL\",\n        \"PT\",\n        \"RO\",\n        \"RU\",\n        \"ZH\",\n        \"SK\",\n        \"ES\",\n        \"SV\",\n        \"ZH\",\n        \"TR\",\n        \"UK\"\n      ],\n      \"fileSizeBytes\": \"127112192\",\n      \"sellerUrl\": \"http://airmailapp.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/airmail-your-mail-with-you/id993160329?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2019-01-03T21:23:18Z\",\n      \"releaseNotes\": \"- Bugfix\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"$7.99\",\n      \"genreIds\": [\n        \"6007\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2016-02-01T05:28:19Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.8.14\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 389546852,\n      \"artistName\": \"Bloop S.R.L.\",\n      \"genres\": [\n        \"Productivity\",\n        \"Utilities\"\n      ],\n      \"price\": 7.99,\n      \"description\": \"Airmail is a powerful mail client for Mac, now available for iPhone and iPad.\\n\\nDesigned for the latest generation iOS, it supports 3D Touch, fast document previewing, high quality PDF creation, and native integration with other apps and services for a frictionless workflow. \\n\\nWorkflow customization is at the core, with a rich feature set like snooze, interactive push notifications, and full inbox sync. \\n\\niCloud sync provides a fully ubiquitous experience so that all your accounts and app preferences are synced.\\n\\nBASIC\\n- Support for Gmail, Exchange EWS, IMAP and POP3\\n- Push notifications, with VIP, custom actions, full body preview and custom sounds\\n- Apple Watch app with glance and interactive notifications\\n- Customizable swipes\\n- Threads and single messages\\n- Snooze messages\\n- Bulk editing \\n- iCloud sync between Mac and iOS\\n- Drafts\\n- Aliases\\n- Multiple signatures \\n- Unified inbox\\n- Horizontal layout\\n- 19 languages\\n\\nADVANCED\\n- 3D Touch quick access\\n- 3D Touch Peek and Pop\\n- Spotlight search for documents and messages\\n- Share composer extension\\n- iCloud sync for labels, preferences and accounts across Mac and iOS\\n- Handoff between Mac and iOS\\n- Notifcation Based on Locations \\n**** \\\"Continued use of GPS running in the background can dramatically decrease battery life.\\\"\\n\\nSEARCH AND FILTERS\\n- Online search\\n- Filter by Unread, Starred, Conversation, Today and Smart\\n- Quick access to the messages of one sender\\n- Quick per account single folder access \\n\\nLABELS AND FOLDERS\\n- Full label access\\n- Per single labels sync\\n- Quick access to recent labels\\n- Favorite labels\\n- Full label creation and editing\\n- Document view with rich preview\\n- Unread, Today, Conversations and Contacts\\n\\nCOMPOSER\\n- HTML rich composer\\n- Attachment resizing\\n- Document import from Dropbox, Google Drive and much more\\n- Signature swipes\\n- Composer extension \\n- Online drafts\\n- Send and Archive\\n\\nOPERATIONS\\n- Undo actions\\n- Move mail between accounts\\n- Multiple signature\\n- Operations view\\n- Attachments view\\n- Contacts view\\n- Mark as unread on open\\n\\nCONTACTS\\n- VIP\\n- Google Directory Search\\n- Exchange Global Address List\\n- Contacts Group messages\\n- Auto CC/BCC\\n\\nVISUAL\\n- Profile icons\\n- Highlight subject\\n- Account icons\\n- Account colors\\n- Preview message lines\\n- Remote images\\n\\nACTIONS\\n- Archive \\n- Trash\\n- Snooze defer messages\\n- Move and Labels\\n- Mark as Unread\\n- Mark as Starred\\n- Mark as Spam\\n- To Do, Memo, Done \\n- Send to Calendar\\n- Send to iOS Extension\\n- Create a searchable PDF \\n- Print \\n- Bounce \\n- Redirect\\n- Transfer to a different account\\n- Universal link Mac/iOS\\n- Add to sender to VIP\\n- Empty Trash and Spam\\n- Mark entire mailbox as read\\n- Archive all messages\\n\\nINTEGRATIONS\\n\\nAttachments:\\n- Google Drive\\n- Droplr \\n- Box.com \\n- OneDrive\\n- Dropbox\\n\\nOpen Links in:\\n- Safari\\n- Chrome\\n- Firefox\\n- iCab\\n- Mercury\\n- Safari in-app\\n\\nSend to Apps and Service:\\n- Calendars Invites\\n- Apple Calendar\\n- Apple Reminder\\n- Omnifocus\\n- Todoist\\n- Wunderlist\\n- Fantastical 2\\n- 2DO\\n- Trello\\n- Clear\\n- Evernote\\n- Appigo Todo\\n- The Hit List\\n- Things\\n- Task\\n- Editorial\\n- Draft 4\\n- iA Writer\\n- Code Hub\\n- Things\\n- 1Writer\\n- Delivery\\n- Github \\n- Swipes \\n- Pocket\\n- DevonThink\\n\\n\\n- Business \\nAirmail is also available on Apple B2B store with MDM and AppConfig support, please contact us for more info.\\n\\nURL Scheme:\\nairmail://compose?subject=[subject]&from=[from]&to=[to]&cc=[cc]&bcc=[bcc]&plainBody=[plainBody]&htmlBody=[htmlBody]\\n\\nairmail://compose?subject=Message%20subject&to=joe%40example.com&ann%40example.com&plainBody=Message%20body\\n\\nAirmail does NOT store your messages on our servers.\\nServer processing is very limited and performed only if users enable push notifications.\\n\\nThanks to all the testers on the Slack group that have been involved in the development!\",\n      \"minimumOsVersion\": \"11.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.airmailapp.iphone\",\n      \"trackName\": \"Airmail - Your Mail With You\",\n      \"trackId\": 993160329,\n      \"sellerName\": \"Bloop S.R.L\",\n      \"averageUserRating\": 3.5,\n      \"userRatingCount\": 195\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/0a/c4/a4/0ac4a47d-3682-3f9d-42e0-cdebb95fb0b7/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/1d/5e/0e/1d5e0e33-c28c-72e8-1ffd-11b1422b5c2e/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/bd/18/44/bd184436-b8cf-1ffb-896b-9ecfdbe389db/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/9e/65/b0/9e65b057-5d1d-00a9-17dc-474189a36bd1/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/23/e2/d2/23e2d2ae-11fe-4edd-50c5-d663a4c37fbf/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/47/86/06/47860628-0381-b71d-4249-1e8e2f8cf7e5/source/552x414bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/59/49/9e/59499ece-afe1-ead2-b73c-9de612ade99c/source/552x414bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/62/5c/e0/625ce0dd-4122-a20a-ff71-a39bbd40a397/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/76/5f/3b/765f3bd6-03c3-cc0f-b73c-4e7efb09172b/source/552x414bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/5a/06/ab/5a06abd1-8425-9742-49ba-e7039f7f43c0/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/83/d0/29/83d0290f-7542-7a3f-a535-7cce7d487094/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/83/d0/29/83d0290f-7542-7a3f-a535-7cce7d487094/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/83/d0/29/83d0290f-7542-7a3f-a535-7cce7d487094/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/wetransfer/id485103881?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"Paste by WeTransfer\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"52582400\",\n      \"sellerUrl\": \"http://www.fiftythree.com/paste\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 1,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/paste-by-wetransfer/id1259981327?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-06T20:18:21Z\",\n      \"releaseNotes\": \"Search to quickly find the deck you need\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6000\"\n      ],\n      \"releaseDate\": \"2017-11-30T02:38:49Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.7.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 485103881,\n      \"artistName\": \"WeTransfer\",\n      \"genres\": [\n        \"Productivity\",\n        \"Business\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Paste is where your ideas come together. From strategy decks to design proposals, Paste automatically formats screenshots, videos, and links as beautiful presentations ready to share with a simple link. It's collaboration for today’s most creative teams—fast, flexible, and visual.\\n\\nPRESENT ANYTHING\\nDrag anything into a deck to instantly create slides from screenshots, videos, docs, and links. Add photos, files, or links to embed YouTube videos, Google Docs, or design files. Auto-layout formats everything for beautiful slides with no wasted effort. Frames are smart filters that wrap any media inside a beautiful phone, tablet, or web mockup.\\n\\nFOCUS ON THE BIG IDEA\\nPaste helps you develop your biggest ideas. See everything and make sense of your work in Storyboard View. Create sections and arrange slides to find the flow that brings your story to life. \\n\\nLESS BUSYWORK, MORE TEAMWORK\\nGather input without distractions. Leave a quick reaction or a lengthy comment—vote, flag or mark it “done.” Integrate with Slack to seamlessly pull your deck into your team conversations with share and comment notifications. Set up confidential decks using Slack’s private channels.\\n\\nALWAYS IN SYNC, INSTANTLY SHAREABLE\\nCreate and review in real-time. Paste syncs to the cloud, so you always have your team's latest thinking at your fingertips. Browse, zoom in, and download every image, file, and video in full resolution. When you’re ready, present your deck onscreen, download as a PDF, or post a public view-only link to the world.\\n\\nPASTE ON DESKTOP\\nGet all of the power of the mobile app and more on any computer by visiting pasteapp.com\\n\\nPRICING\\nPaste is free to use for as long as you need it. Upgrade to get access to more decks, customization, and control over sharing. Learn more at fiftythree.com/paste/pricing\\n\\nIf you need help or want to share feedback, please contact us at support@pasteapp.com\",\n      \"minimumOsVersion\": \"11.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.fiftythree.paste\",\n      \"trackName\": \"Paste by WeTransfer\",\n      \"trackId\": 1259981327,\n      \"sellerName\": \"FiftyThree, Inc.\",\n      \"averageUserRating\": 4.0,\n      \"userRatingCount\": 47\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/b6/27/89/b627896b-1ef2-92cd-ae8e-ef7eba3ca0e6/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple4/v4/8f/aa/26/8faa2607-82f1-2355-b668-380a8572bc76/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple2/v4/74/e2/2d/74e22dea-e110-92d6-5d1e-c95fda4b4c04/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple49/v4/94/66/81/94668130-f8c0-981f-af1e-4a258ca7f514/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/4b/12/98/4b129841-0b22-5680-4c63-48287149a3f5/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/4b/12/98/4b129841-0b22-5680-4c63-48287149a3f5/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/4b/12/98/4b129841-0b22-5680-4c63-48287149a3f5/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/copper-technologies-inc/id986907147?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"OneChannel for Slack\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"14772224\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/onechannel-for-slack/id1064748228?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2016-01-12T22:07:02Z\",\n      \"releaseNotes\": \"A few important bytes to helps us better understand how folks use OneChannel so we can make it better.  We threw in a little bonus, too: select text before using the Action Extension and we'll include the quote in your post.\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2015-12-18T18:20:12Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.3\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 986907147,\n      \"artistName\": \"Copper Technologies, Inc.\",\n      \"genres\": [\n        \"Productivity\",\n        \"Utilities\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"OneChannel is the fastest way to post to Slack from anywhere on iOS. Connect to any channel or user and send to Slack in one tap.\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.withcopper.OneChannel\",\n      \"trackName\": \"OneChannel for Slack\",\n      \"trackId\": 1064748228,\n      \"sellerName\": \"Copper Technologies, Inc.\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/3c/f7/e4/3cf7e461-f683-ee69-2b9b-75d0d8bcc7e9/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/70/80/4a/70804a4d-1f10-4b40-784b-0e03c632b82b/source/406x228bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/2f/52/3d/2f523d17-9486-c15e-1374-4ceea9a557b0/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/91/5e/12/915e1209-9f99-8d2a-44a3-79728265c9df/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/13/85/d8/1385d820-f9ad-2c0c-b87b-7ed5046aaee7/source/552x414bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/bd/b6/4f/bdb64f70-4e9b-fcc0-02f0-515f8de55aaa/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/55/5a/49/555a49e8-9ef6-0663-f18f-c450b095a9f5/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/b6/0f/55/b60f555b-fe62-5b6e-0457-2faf7d3ad1d4/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/89/95/93/8995934e-b99c-1db6-ff56-6b7e1c221bd2/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/89/95/93/8995934e-b99c-1db6-ff56-6b7e1c221bd2/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/89/95/93/8995934e-b99c-1db6-ff56-6b7e1c221bd2/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/colloquy-project/id302000481?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Colloquy - IRC Client\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"DE\",\n        \"IT\",\n        \"KO\"\n      ],\n      \"fileSizeBytes\": \"10020864\",\n      \"sellerUrl\": \"http://colloquy.mobi\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/colloquy-irc-client/id302000478?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-04-01T17:43:44Z\",\n      \"releaseNotes\": \"Changes:\\n- Improve reliability when registering for push notifications\\n- Small performance improvements when connecting to bouncers\\n\\nFixes:\\n- Fix issue where chat previews could disappear\",\n      \"primaryGenreId\": 6005,\n      \"formattedPrice\": \"$2.99\",\n      \"genreIds\": [\n        \"6005\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2009-01-17T07:44:49Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.9.2\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 302000481,\n      \"artistName\": \"Colloquy Project\",\n      \"genres\": [\n        \"Social Networking\",\n        \"Utilities\"\n      ],\n      \"price\": 2.99,\n      \"description\": \"Colloquy for iPad, iPhone and iPod touch puts the power of the most popular IRC client for the Mac in the palm of your hand. Built atop the Chat Core framework, Colloquy Mobile is a full featured client optimized for the on-the-go experience with iOS multitasking support.\\n\\nUnique Features:\\n• Support for retina devices, including iPhone 6s, iPhone 6s Plus and iPad Pro.\\n• Support for iOS multitasking with local notifications and split-screen support.\\n• Push notifications when using a compatible push bouncer.\\n• Convenient nickname and emoticon completion popups.\\n• Support for all the common IRC commands with completion.\\n• Organized Colloquies view that shows all your conversations and rooms at a glance.\\n• Highlights messages (and optionally vibrates) when your specific words or nickname is mentioned.\\n• Highly customizable interface and behavior settings within the Settings application.\\n• Visual display of user information (WHOIS) for any user.\\n• Full support for landscape mode in the entire application.\\n• Stays connected while iPhone is locked and when SMS alerts appear.\\n• Searchable room member list.\\n• Support for SASL authentication (required when connecting to Freenode over the cell network.)\\n• A console for every connection.\\n• Support for ignoring annoying users.\\n\\nStandard Features:\\n• Multiple message styles to choose from.\\n• Fully compatible with mIRC colors and formatting.\\n• Large selection of graphical emoticons.\\n• Allows you to join multiple chat rooms across many different servers.\\n• Automatic identification with network services (NickServ).\\n• Notification of common server errors as easy to understand alerts.\\n• Automatically join rooms and send commands upon connect.\\n• Solid support for secure connections over SSL and TLS.\\n• Full support for room and connection text encodings.\\n• Full IRCv3 compatibility, including the IRCv3.2 standard.\\n• Open minded and Open Source, like it should be.\\n\\nATTENTION:\\nWe can't provide support here to people leaving reviews, as much as we want to. We also can't discuss feature requests, since there is no way to reply to you here. If you have a problem or suggestion, please visit us in #colloquy-mobile on irc.freenode.net, click the support link below, or email us at support@colloquy.mobi.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Social Networking\",\n      \"bundleId\": \"info.colloquy.mobile\",\n      \"trackName\": \"Colloquy - IRC Client\",\n      \"trackId\": 302000478,\n      \"sellerName\": \"Jane Lee\",\n      \"averageUserRating\": 3.5,\n      \"userRatingCount\": 134\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/ea/81/e0/ea81e017-d647-8f42-702c-0a1ef609e3f3/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/54/fe/18/54fe1851-ccc0-43a3-faae-4a6a88f67ed8/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/ba/3f/46/ba3f46db-74f7-a220-33e0-68d973c5acfd/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/ca/ae/08/caae087f-1294-385d-2f78-2e7e1847de5f/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/48/4d/d9/484dd942-c72e-8cdd-54ef-dee85b614719/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/9b/bd/e8/9bbde842-506f-0999-0480-14d55b3077c8/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/67/84/a4/6784a4ba-cf72-5718-f3a7-5c4a091ec7e5/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/70/64/3d/70643dc9-a70b-8bf2-72c0-1305385c70cc/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/c5/82/cc/c582cc38-1359-6810-1921-f15621c2a90d/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/c5/82/cc/c582cc38-1359-6810-1921-f15621c2a90d/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/c5/82/cc/c582cc38-1359-6810-1921-f15621c2a90d/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/alibaba-com-hong-kong-limited/id436672032?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"AliSuppliers - App for Alibaba\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"ZH\",\n        \"ZH\"\n      ],\n      \"fileSizeBytes\": \"224596992\",\n      \"sellerUrl\": \"http://mobile.alibaba.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/alisuppliers-app-for-alibaba/id708064914?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2019-01-07T19:25:55Z\",\n      \"releaseNotes\": \"1. Fixed some known issues.\\n2. Other performance optimizations and experience improvements.\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6024\"\n      ],\n      \"releaseDate\": \"2013-10-27T19:25:22Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"9.6.3\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 436672032,\n      \"artistName\": \"Alibaba.com Hong Kong Limited\",\n      \"genres\": [\n        \"Business\",\n        \"Shopping\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"----Manage Your Business, Any Time, Anywhere----\\n√ An official Alibaba product\\n√ Provides services for businesses on Alibaba.com, and is an essential app for any e-commerce business\\n√ Dedicated to providing solutions for businesses and increasing operational efficiency\\n----Basic Description----\\nThe official Alibaba Supplier app serves as a dedicated e-commerce store operation, business management, and information and communication mobile tool for businesses all over the world. With Alibaba Supplier, you can easily manage store merchandise and orders, check shop data and messages, process quotes, and take advantage of business opportunities anytime, anywhere, allowing you to best manage your valuable time and better interact with potential buyers.\\n----Main Features----\\n[Workbench] Provides core operational data and a personalized operation page that includes plugins for things like products, order transactions, member management, inquiries and requests for quotation (RFQ). Shop owners can add or delete plugins freely to build their own personalized workbench.\\n[Messages] Notification messages for the receipt of goods, orders, business opportunities and campaigns.\\n[Chat] Inquiries from buyers can be answered in the blink of an eye. Alibaba Supplier supports a computer and mobile device being logged into the same account at the same time so that no transactions will be missed. There are also several ways to chat such as voice and video chat, and you can also check the read/unread status of messages and quickly understand what the buyer wants.\\n[Services] Provides e-commerce services to businesses and increases their operational efficiency.\\n[Headlines] Abundant and up-to-date e-commerce news with diverse content such as official laws, marketing strategies, high-quality products and live videos.\\n\\n----Contact Us----\\nIf you encounter any problems while using Workbench, please contact us at:\\n-   Online assistance: Go to Qianniu > Me > Settings > Questions and Feedback\\n-   PC official website: www.alibaba.com\\n-   Mobile phone official website: m.alibaba.com\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.alibaba.icbu.app.seller\",\n      \"trackName\": \"AliSuppliers - App for Alibaba\",\n      \"trackId\": 708064914,\n      \"sellerName\": \"Alibaba.com Hong Kong Limited\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple115/v4/f3/7d/28/f37d2835-bf88-45af-96b4-e7b915788bc7/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple125/v4/79/9e/ef/799eefbf-25a8-26d0-f79f-87064c2198dd/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple115/v4/3b/db/42/3bdb4291-b2c7-f18a-1ad7-100db460af00/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple115/v4/6e/5f/04/6e5f0428-64ed-7469-a104-93c91e18cfc3/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple125/v4/4d/e6/3b/4de63b1f-52e1-f3e0-1859-cc2bec7dee0e/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple125/v4/89/98/5a/89985a94-79e4-21eb-9a83-9fee6f1cceac/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple115/v4/a2/5f/8a/a25f8aa9-edbb-0abb-f875-92786be7d143/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple115/v4/de/50/57/de505787-6d36-eb2d-734d-7ca1ef3c0900/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple125/v4/21/0a/f6/210af6c9-64e8-43c1-acf7-08dffb459e1b/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple125/v4/24/3f/3a/243f3af5-763a-7bc0-59dc-dbaad439fff1/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/cd/a1/6e/cda16e41-a841-dc4c-7018-d2e66f47be4d/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/cd/a1/6e/cda16e41-a841-dc4c-7018-d2e66f47be4d/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/cd/a1/6e/cda16e41-a841-dc4c-7018-d2e66f47be4d/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/moxtra-inc/id551221476?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Moxtra: Business Collaboration\",\n      \"languageCodesISO2A\": [\n        \"DA\",\n        \"NL\",\n        \"EN\",\n        \"FI\",\n        \"FR\",\n        \"DE\",\n        \"ID\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"PT\",\n        \"RU\",\n        \"ZH\",\n        \"ES\",\n        \"SV\",\n        \"TH\",\n        \"ZH\",\n        \"TR\",\n        \"VI\"\n      ],\n      \"fileSizeBytes\": \"108129280\",\n      \"sellerUrl\": \"http://www.moxtra.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/moxtra-business-collaboration/id590571587?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-11-16T13:04:36Z\",\n      \"releaseNotes\": \"- Fixed bugs\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2013-01-27T08:00:00Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"5.2.16\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 551221476,\n      \"artistName\": \"Moxtra, Inc.\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Moxtra: Accelerating business in the mobile world.\\n\\nMoxtra is a collaboration solution built to accelerate business. Present, secure feedback, get approvals on content to close business while on the go. Collaborate on documents and content across teams, with customers, partners, and colleagues. Recreate the power of face-to-face meetings with secure messaging, robust document collaboration, video conferencing, electronic signature, and more – in context. Moxtra is an secure, enterprise class service available as a white-label, private cloud, or on-premise solution.\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.moxtra.moxtra\",\n      \"trackName\": \"Moxtra: Business Collaboration\",\n      \"trackId\": 590571587,\n      \"sellerName\": \"Moxtra, Inc.\",\n      \"averageUserRating\": 4.0,\n      \"userRatingCount\": 20\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/f9/ee/f1/f9eef16a-490a-aecb-8bee-31791513a84e/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple7/v4/34/8e/db/348edbcf-bd89-346b-cc97-ffc2009746f6/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple1/v4/01/50/b1/0150b1bd-69c1-34e8-08b4-fbc42e1ade47/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple1/v4/bf/f1/e1/bff1e1d0-c8ec-ecd9-6743-98dffa534294/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/5c/13/30/5c133045-2c24-52f0-1b9a-6e935cafaae6/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple5/v4/c8/9a/80/c89a80de-e5fc-b37a-cb42-a77eb7829dc5/source/360x480bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple7/v4/42/b7/66/42b76637-de47-fbd5-b3c4-9ee14dd70001/source/360x480bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/3c/e9/8f/3ce98f1d-713d-7e2e-281e-f68e814e3384/source/360x480bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple1/v4/63/e0/1a/63e01a54-0e2c-cf74-0afc-39b465e6279e/source/360x480bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple7/v4/36/73/06/367306a5-4c3f-2089-4a59-8e3e6c227a2c/source/360x480bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/3e/e7/99/3ee7999a-ad78-5790-74b2-9b4fb7c39b92/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/3e/e7/99/3ee7999a-ad78-5790-74b2-9b4fb7c39b92/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/3e/e7/99/3ee7999a-ad78-5790-74b2-9b4fb7c39b92/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/linebreak/id417602907?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 4.5,\n      \"trackCensoredName\": \"Annotate - Text, Emoji, Stickers and Shapes on Photos and Screenshots\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"65638400\",\n      \"sellerUrl\": \"https://annotate.driftt.com/annotate\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 34,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/annotate-text-emoji-stickers-shapes-on-photos-screenshots/id994933038?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2015-07-29T20:58:01Z\",\n      \"releaseNotes\": \"Your feedback is life. It’s the air that we breathe, the wind at our backs, the sun on our faces after a long, cold, dark night.\\n\\nYou are our sunshine. You make us happy when skies are grey.\\n\\nThis latest release makes it easier than ever for you to share your warm thoughts and feedback whenever the mood strikes you.\\n\\nTap the little question mark bubble to send us feedback via Twiitter, email, or text message. Whatever works for you.\\n\\nLove your latest work of Annotate art? Tweet it to us @getannotate. We promise to love you back. <3\\n\\nPlease don’t take our sunshine away.\",\n      \"primaryGenreId\": 6008,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6008\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2015-05-30T02:09:51Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.5\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 417602907,\n      \"artistName\": \"Linebreak\",\n      \"genres\": [\n        \"Photo & Video\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"\\\"This app is a must have” — David Wiltson\\n\\nAnnotate is the simplest way to capture, annotate and save or share photos and screenshots.\\n\\n\\nFEATURES\\n\\nSnap a photo or select an image from your camera roll, then dress it up with stickers and annotate it with arrows, text, and the pen tool. Or use the pixelate tool and built-in emojis for maximum impact.\\n\\nAdd a caption and share it with friends on your favorite apps, including Apple Messages, Mail, Twitter, Slack, Snapchat, WhatsApp, Line, Instagram and Facebook.\\n\\nQuickly and easily redact parts of an image.\\n\\nHave fun with the 100's of emoticons built-in and ready for you to add to your photos and screenshots. Full support for landscape mode on iPad and iPhone, so rotate away!\\n\\n\\nREVIEWS\\n\\n\\\"Amazing ideas came up as soon as i started using this app.\\\" — Eredis2\\n\\n“It’s a great little alternative to a snapchat, and let’s me describe photos to anyone.\\\" — Majickdave\\n\\n\\\"Intuitive and very helpful for collaboration.\\\" — PCampbell\\n\\n\\\"This app is a must have, you can do all annotations that you need with great quality and intuitive controls. Great! ” — David Wiltson\\n\\n\\\"Best way to mark up your screenshots and photos, great for client work or even to remind yourself later when editing photos\\\" - Smbnyc\\n\\n\\nSUPPORT\\n\\nIf you have any questions or feedback we’d love to hear from you! Driftt offers free support, you can reach us by email at annotate@driftt.com or on Twitter @DrifttHQ. \\n\\nYou can also browse our FAQs and User Guides on http://use.driftt.com/.\\n\\nThank you!\\n\\nWe have lots of great plans for future versions, so please leave us feedback and rate us in the App Store!\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Photo & Video\",\n      \"bundleId\": \"com.driftt.ios.Annotate\",\n      \"trackName\": \"Annotate - Text, Emoji, Stickers and Shapes on Photos and Screenshots\",\n      \"trackId\": 994933038,\n      \"sellerName\": \"Linebreak SL\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 36\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/4e/be/73/4ebe7322-25da-bb3f-516b-8a65896a4a81/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/d7/6f/b4/d76fb430-6b95-84e7-5a02-1589f0e5f69d/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/f0/5f/54/f05f54f1-541e-2490-d6f8-62787832eff3/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/5b/ae/c3/5baec325-229c-6b4b-5e59-72636a54b4a0/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/9c/b7/98/9cb79895-3f62-7e16-046e-b5db2c919200/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/18/38/d9/1838d903-dfd1-e91d-9e84-3da6ade92048/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/18/38/d9/1838d903-dfd1-e91d-9e84-3da6ade92048/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/18/38/d9/1838d903-dfd1-e91d-9e84-3da6ade92048/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/mailtime/id914281818?uo=4\",\n      \"advisories\": [\n        \"Unrestricted Web Access\"\n      ],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"MailTime Email Messenger\",\n      \"languageCodesISO2A\": [\n        \"AR\",\n        \"CA\",\n        \"HR\",\n        \"CS\",\n        \"DA\",\n        \"NL\",\n        \"EN\",\n        \"FI\",\n        \"FR\",\n        \"DE\",\n        \"EL\",\n        \"HE\",\n        \"HI\",\n        \"HU\",\n        \"ID\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"MS\",\n        \"NB\",\n        \"PL\",\n        \"PT\",\n        \"RO\",\n        \"RU\",\n        \"ZH\",\n        \"SK\",\n        \"ES\",\n        \"SV\",\n        \"TH\",\n        \"ZH\",\n        \"TR\",\n        \"UK\",\n        \"VI\"\n      ],\n      \"fileSizeBytes\": \"101133312\",\n      \"sellerUrl\": \"http://mailtime.com\",\n      \"contentAdvisoryRating\": \"17+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/mailtime-email-messenger/id914281815?mt=8&uo=4\",\n      \"trackContentRating\": \"17+\",\n      \"currentVersionReleaseDate\": \"2018-12-22T00:10:50Z\",\n      \"releaseNotes\": \"Happy Holidays!\\n\\nBug fixes.\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6005\"\n      ],\n      \"releaseDate\": \"2014-09-08T07:00:00Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.0.4\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 914281818,\n      \"artistName\": \"MailTime\",\n      \"genres\": [\n        \"Productivity\",\n        \"Social Networking\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"\\\"Best of 2015\\\" by Apple\\nMailTime is an open mobile messenger built with email technology. It’s email as quick and easy as texting, and messaging without forcing all your contacts to download the same app. \\n\\nWe reformat your cluttered email threads into clean bubble conversations and separate the important human messages in your inbox from the discounts and newsletters.\\n\\nMailTime supports multiple email accounts, as well as Gmail, iCloud, Yahoo, Outlook, AOL, Office 365, Mail.ru, Hotmail, QQ,163, 126, Tencent Enterprise, Google Apps Mail services. You can also attach files from Dropbox, iCloud, Google Drive, Box, and One Drive with MailTime. \\n\\nUPDATE: Now users can search within MailTime from any screen using the Spotlight Search. Jump into a new email with 3D Touch, Peek into emails before opening, Swipe a quick reply, or Long Press to expand email bubbles, addresses, contacts and web links with ease.  \\n\\n------------\\nFeatures\\n\\nEmail Messaging:\\nOur content parsing engine cuts out annoying metadata to display emails in clean bubbles. View your emails as conversations, not threads!\\n\\nCommunicate, Don't Organize:\\nOur intelligent inbox sorts out the Important humans from the newsletters, discounts, and other machine-generated mail in All Mail. Talk to people you care about, not machines!\\n\\nGroup Chats:\\nManaging your conversations in MailTime is just like a group chat. To add, remove, or switch participants to 'cc' or 'bcc', just swipe left and change your participants' status. \\n\\nOne Click To-dos:\\nUse the @ symbol to quickly assign tasks without leaving the inbox.  For example, \\\"@Charlie, Please download MailTime\\\", will put the rest of that sentence into Charlie's Mentions list, next to the group status list.\\n\\nToo Long; Didn't Read:\\nJust like Twitter prevents you from writing more than 140 characters, MailTime alerts you if your message is too long. You can still send them, but no guarantees that they'll be read!\\n\\n------------\\nFAQ\\n\\n“What does it look like to someone who doesn't have the app?”\\nTo non-MailTime users, messages appear as normal emails. If you'd like to view the original email within MailTime, you can tap on the corresponding bubble.\\n\\n“What about long emails and attachments?”\\nWe've noticed that most people save their essays for desktop and leave quick responses for mobile, but we've got you covered. MailTime shortens all messages to fit the screen, and to see the rest, simply tap that bubble. Attachments are visible on the bottom of the bubble and similarly accessible.\\n\\n“Can I swipe away all my emails in MailTime?”\\nWe don’t treat your inbox as a task list. That’s what Mentions are for - tag a recipient with the @ symbol to assign the rest of the sentence to that person’s task list. However, you can Mark as Read, Archive, and Delete all with easy swipes!\\n------------\\nAs featured in Forbes, Business Insider, CNBC, LifeHacker, Fox, and TC Disrupt Startup Battlefield 2014.\\n\\nWe love emails! Talk to us anytime by clicking the “Write to MailTime Team” button or send an email to support@mailtime.com\\n\\nFollow us on Twitter at @mailtimeapp, \\nlike us on Facebook at /mailtimeapp or \\nvisit our website mailtime.com\\n\\nHave A Good MailTime!\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.mailtime.MailTime\",\n      \"trackName\": \"MailTime Email Messenger\",\n      \"trackId\": 914281815,\n      \"sellerName\": \"Mobile Internet Limited\",\n      \"averageUserRating\": 4.0,\n      \"userRatingCount\": 9\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/09/87/0a/09870a86-2748-884e-f7d0-3c812379399e/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/40/ce/b1/40ceb117-c5fc-1249-cb22-83e19d5415d1/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/1e/ee/a9/1eeea9de-a65d-6dd3-4e04-9379b1d9f38b/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/dc/57/95/dc57957d-20ec-4156-04f8-db56c681bf6b/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/55/9b/ec/559bec7b-94ae-7a1e-4b64-90769ddaf80c/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/0b/a4/f3/0ba4f3b6-de86-ba0e-767a-689d3fb26d65/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/d7/1a/64/d71a6454-429b-497d-9af4-1fd9a6d28b2f/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/58/58/fa/5858fab3-d8f7-2c62-f736-7caca3cb8aa7/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/46/e0/52/46e052b5-c3a1-aeef-e3a0-f3e6216d65b3/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/16/f2/70/16f270b1-4668-cec9-2742-5df6abdbdb24/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/6e/e0/4e/6ee04ea5-e101-ff57-0ea1-5c1e83a9c058/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/6e/e0/4e/6ee04ea5-e101-ff57-0ea1-5c1e83a9c058/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/6e/e0/4e/6ee04ea5-e101-ff57-0ea1-5c1e83a9c058/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/slikr/id1083307348?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Spasifik Cuts Barber\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"45223936\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/spasifik-cuts-barber/id1355242933?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-07-29T23:21:37Z\",\n      \"releaseNotes\": \"Improved alerts\\nGeneral updates and improvements\",\n      \"primaryGenreId\": 6012,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6012\"\n      ],\n      \"releaseDate\": \"2018-03-09T14:58:49Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.2\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1083307348,\n      \"artistName\": \"SLIKR\",\n      \"genres\": [\n        \"Lifestyle\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Spasifik Cuts Barbershop is located in Slacks Creek, Brisbane. Brisbane's original urban Barbershop for over 13 years.  Join our queue online and reduce your wait time.\\n\\nSee the wait time for each barber\\nCheck-in online\\nSecure your place\\nTrack progress online\\nCancel online if needed\\n\\nWe respect your time so check-in now and beat the wait in the shop.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Lifestyle\",\n      \"bundleId\": \"com.slikr.spasifik\",\n      \"trackName\": \"Spasifik Cuts Barber\",\n      \"trackId\": 1355242933,\n      \"sellerName\": \"SLIKR PTY LTD\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/f3/d2/ee/f3d2ee2a-a819-0251-c20f-e1900cce874d/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/ba/e4/2c/bae42cd7-9a0e-b896-2703-f4d442b1a5e5/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/f7/ea/ee/f7eaee7c-6bef-1e03-e514-a4637dab25e9/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/ec/c8/f3/ecc8f322-96db-9bfb-9378-f90bb583a221/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/95/68/ff/9568ff15-e5b4-9117-2764-678c246d686e/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/b2/63/4f/b2634fed-a626-0fb5-5234-79cee7089733/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/68/81/56/6881568a-ebe1-e073-4ff0-7fe997fb75b3/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/31/2c/1b/312c1b48-b7d6-1228-2796-f627bc1b1c5d/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/c0/25/6a/c0256a0f-d705-0093-68f3-0910da8f004d/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/1a/7e/a4/1a7ea443-1a6a-bfda-62a0-975481291dd8/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/af/dc/46/afdc4638-0103-f987-e58d-2e70ba51a8c1/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/b3/da/b7/b3dab70b-b119-ccd3-b2a7-72abed7abde9/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/50/ba/58/50ba58eb-227b-3dd5-0a8d-29df486e2fdf/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/50/ba/58/50ba58eb-227b-3dd5-0a8d-29df486e2fdf/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/50/ba/58/50ba58eb-227b-3dd5-0a8d-29df486e2fdf/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/jus/id1403744826?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"JusTalk Kids - Safe Video Chat\",\n      \"languageCodesISO2A\": [\n        \"AR\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"HI\",\n        \"ID\",\n        \"JA\",\n        \"KO\",\n        \"PT\",\n        \"RU\",\n        \"ZH\",\n        \"ES\",\n        \"ZH\",\n        \"TR\",\n        \"VI\"\n      ],\n      \"fileSizeBytes\": \"69033984\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/justalk-kids-safe-video-chat/id1403744827?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2019-01-01T02:40:04Z\",\n      \"releaseNotes\": \"Latest Updates:\\n- Improved parental control: parents or guardians now can control kids’ access to all social features by setting passcode on the app.\\n- Bug fixes and product improvements.\\n\\nThank you for using JusTalk Kids! If you have any questions, concerns, or suggestions, please feel free to contact with us via email: kids@justalk.com\",\n      \"primaryGenreId\": 6005,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6005\"\n      ],\n      \"releaseDate\": \"2018-08-01T10:33:28Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"0.4\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1403744826,\n      \"artistName\": \"Jus\",\n      \"genres\": [\n        \"Social Networking\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"JusTalk Kids is a FREE video calling and messaging app designed for kids to connect with family and close friends from their tablets or smartphones.\\n\\n• Safe Environment for Kids\\n- Parents or guardians can control kids’ access to all social features in the app.\\n- NO harassment from strangers. Kids will not receive friend requests, messages, or calls from strangers unless they add the parents-approved person first.\\n- No phone number is needed. Parents and guardians help their kids to create a JusTalk Kids account by setting a JusTalk ID through the bottom of the signup page.\\n- Your child’s JusTalk ID won’t be recommended to any other users on JusTalk or JusTalk Kids!\\n- There are no ads or in-app purchases and the app is free to download.\\n\\n• More Fun for Kids\\n- Doodles, stickers, photo sharing: help kids creatively express themselves and learn new things during calls!\\n- Voice and video recording: save memorable childhood forever!\\n- Play fun and interactive game while calling with parents or close friends.\\n- Start a live video chat to help kids share their favorite moments with loved ones in voice calls.\\n- Send and receive photos, instant video messages, emoji and more on chats.\\n\\n• Works with JusTalk\\n- Your child uses JusTalk Kids to video call and message over Wi-Fi or on-the-go (EDGE/2G/3G/4G)*.\\n- Parents and other family members can chat with their kids through their existing JusTalk app.\\n\\n• Cross-platform\\nJusTalk Kids is supported to use on different operating systems and all kinds of device sizes no matter it’s a smartphone or tablet.\\n\\n• Private and Secure\\nAll your children's personal information (including calling and messaging data) is end-to-end encrypted. It's split into multiple random path which ensures it can’t be monitored or saved by servers. \\n\\n*Data charges may apply. Check with your carrier for details.\\n\\n-----------------------------------------------------\\nWe're always excited to hear from you! Please feel free to contact us via:\\nEmail: kids@justalk.com\\nFacebook: facebook.com/justalkkids\\nTwitter: @JusTalkKids\\nInstagram: @justalk_kids\\n-----------------------------------------------------\\n\\nThe following is a more detailed description:\\n\\n• Best Kid-Friendly App\\nJusTalk Kids is the best video chat and messenger kids and their families have been looking for! \\n\\n• Free Phone Calls and Kids Chat\\nMake voice or lively video chats with your children wherever you are. You will never miss any important moment with your family. When you’re unable to accompany you kids, experience life events (loss of first tooth, the first school day, a birthday house party and so on) with your kids together on JusTalk Kids with no distance.\\n\\n• Interactive Video Features\\nDuring video chat you can tell short stories to your kids by sharing their favorite pictures of story books. Parents and kids doodle together and add emoticons or stickers to spark conversation and laughter. Play games and compete with each other.\\n\\n• Designed for Family\\nJusTalk Kids is designed as an easy-to-use, safe and educational tool for children where family and friends meet together. Make free calls and share free messages with family and kids.\\n\\n• High Quality Wi-Fi Calling App\\nEnjoy crystal-clear call quality, video chat and free texting. Using JusTalk Kids to call free or send a text now to your kids.\\n\\n• Capture the Moment\\nQuickly snap a photo or video and send to chats. It's not only for kids and their family, but also is a hangout for kids and their close friends.\\n\\n• Simple Interface\\nA clean, intuitive design makes communicating faster and more fun.\\n\\n• Low Data Usage\\nSave 40-90% of the voip or vowifi network traffic during realtime video chat with 720P HD quality. Like a fun video walkie talkie.\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Social Networking\",\n      \"bundleId\": \"com.justalk.kids.ios\",\n      \"trackName\": \"JusTalk Kids - Safe Video Chat\",\n      \"trackId\": 1403744827,\n      \"sellerName\": \"Ningbo Jus Internet Technology Co., Ltd.\",\n      \"averageUserRating\": 4.0,\n      \"userRatingCount\": 9\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/dc/60/54/dc6054be-4707-d3af-d2a4-5c7ab85b4000/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/b3/d4/28/b3d42823-a787-7b8a-218f-cf1af74b4a68/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/c1/64/96/c1649638-d404-58db-5d23-8209c434b855/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/9b/0f/85/9b0f859d-e757-43f3-fc89-9f1f1c6206e3/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/84/c8/9c/84c89c11-ba6a-1547-d9f2-2d84d88a2f62/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/e5/93/7c/e5937c2e-7ebf-62e4-65f2-bd35eede318e/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/f6/e1/d5/f6e1d52b-e0a4-6222-cddf-282ddbebb736/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/d0/bb/7c/d0bb7c6b-fb2c-dc3b-78b3-cafd3168a201/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/af/ee/c9/afeec9a1-ecee-265a-d39d-267862496006/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/2d/f1/cb/2df1cb0c-ffdb-22ea-7a4f-30ea77fa854b/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/dd/0f/1e/dd0f1ebf-ae8b-3a33-a664-03c489a2589b/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/32/05/4b/32054b85-04c9-01d5-4d5f-d084eea8b1a0/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/8c/f8/76/8cf876ce-6b5d-bc8f-fb49-0587b2ef215f/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/a7/ab/9a/a7ab9a7c-5f26-c6f8-e701-f28626e2f48c/source/576x768bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/47/3d/f9/473df90c-324c-2051-f958-e5e0d92af659/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/6f/7a/8e/6f7a8e27-5027-6ad1-4d2b-ee968eb51e69/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/57/2b/4a/572b4a12-ef3c-8527-2c7e-a5a7d9f72fb4/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/44/05/5c/44055c39-a87f-9de6-f077-9254eb147279/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/44/05/5c/44055c39-a87f-9de6-f077-9254eb147279/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/44/05/5c/44055c39-a87f-9de6-f077-9254eb147279/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/riva-fzc/id503815096?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Flock: Team Communication App\",\n      \"languageCodesISO2A\": [\n        \"CA\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"PT\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"82672640\",\n      \"sellerUrl\": \"http://flock.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/flock-team-communication-app/id879007584?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-19T06:13:06Z\",\n      \"releaseNotes\": \"Presence information for team members is now more accurate than ever so you can see if a colleague is online, away or even unreachable.\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2014-05-29T00:08:01Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"2.34.3\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 503815096,\n      \"artistName\": \"RIVA FZC\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Flock is a messaging app for teams. Packed with tons of productivity features, Flock drives efficiency and boosts speed of execution.\\n\\nIt lets you connect with your team, get on video calls, manage projects with to-dos, polls and reminders and integrate your most favourite apps. All of this and more over a beautiful interface!\\n\\nFlock is already being loved by teams in over 25,000 organisations across the world.\\n\\nHere’s why you would too:\\n\\n* Public channels to discover and connect with like-minded individuals\\n* Invite-only private channels for more focussed discussions\\n* Real time direct and channel messaging, synced across devices\\n* Video and audio calling with hassle free screen sharing capabilities\\n* File sharing on personal chats and channels\\n* Creating polls and to-dos from your desktop and mobile app\\nIntegrations with your favourite tools like Trello, Twitter, Hubot and GitHub\\n\\nFlock is free to use for as many users and for as long as you want. You can upgrade to our paid plans for more features and increased user control.\\n\\nYou’re in good company.\\n- Tim Hortons\\n- Avendus\\n- Gini and Jony\\n- Ricoh\\n- Victorinox\\n\\nAnd here’s what they have to say about Flock:\\n\\n“I can’t say enough kind things about Flock. Moved the team to it from Slack and couldn’t be happier.”\\n- Luke Rodriguez, Modern Horrors\\n\\n“Flock is convenient and real time and is making communication seamless and easy. My entire team today is on Flock.”\\n- Prashant Tandon, CEO and Co-Founder, 1MG\\n\\n“Flock has become the way our Sales Team communicates. It's fast, reliable, fun and easy to use.”\\n- Bryan Morales, CIB Corporation\\n\\nFollow us on Facebook and Twitter @flock\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"to.talk.goto\",\n      \"trackName\": \"Flock: Team Communication App\",\n      \"trackId\": 879007584,\n      \"sellerName\": \"RIVA FZC\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/3b/b6/f0/3bb6f08a-51a9-52cc-6fb9-0442d8c51d50/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/c0/1f/34/c01f34db-b784-313d-de99-0e76f7352bd2/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/57/d2/3a/57d23a2a-08d8-0c9c-0d53-0be7ece01f1d/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/a8/b4/7d/a8b47d00-6a7f-9ebf-23cf-0d6d2100daa7/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/0c/f2/ad/0cf2ad8f-5c24-3766-5e37-e7485efde6a8/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/e4/00/3b/e4003b0f-d245-5865-c851-8ed10453e0ae/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/e4/3a/c1/e43ac1fe-2dd2-ddc0-6184-ae4b0f083ce7/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/e4/3a/c1/e43ac1fe-2dd2-ddc0-6184-ae4b0f083ce7/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/e4/3a/c1/e43ac1fe-2dd2-ddc0-6184-ae4b0f083ce7/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/hopflow/id491044400?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Spike Email (Formerly Hop)\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"55273472\",\n      \"sellerUrl\": \"https://www.spikenow.com/\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/spike-email-formerly-hop/id707452888?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-17T06:09:52Z\",\n      \"releaseNotes\": \"* All photo attachments are now in high resolution !\\n* iPad multitasking is now supported \\n* Improved camera both for stills and video\\n* Camera roll previews are now high quality \\n* Added support for the new iPad Pro, iPhone XR & iPhone XS Max\\n* resolved some annoying bugs and hangs\\n\\nPS. Don’t panic - it’s just a name change!\\nHop is now Spike. And, the app you love is now on ALL devices: Mobile & Desktop\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6000\"\n      ],\n      \"releaseDate\": \"2013-10-12T07:00:00Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"2.6.4\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 491044400,\n      \"artistName\": \"Hopflow\",\n      \"genres\": [\n        \"Productivity\",\n        \"Business\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Spike upgrades the way you work, saving you and your team time, sanity, and a lot of headache. Cutting the distractions of old-fashioned inboxes, Spike gives you the cleanest, fastest productivity tool, all from within your inbox. \\nSpike is available on ANY device-  access your clients, teams, messages, and all your files, email and cloud accounts, in a single place.\\n\\nSpike is the world’s first conversational email app. \\nDesigned for everyone. Made for teams. Tools you actually need.\\n\\n******************************\\n“Client communications have become a pleasure thanks to Spike. It’s faster, more efficient, and more fun to use.” - Mosey L. \\n“Spike helps me respond faster than any other app.” - Bertrand M.\\n“Once horrifying email threads are now short and focused conversations. We’re addicted to it now!” - Max S.\\n“It's effortless and saves a lot of time. It’s genius.” - Jozsef J. \\n \\n******************************\\n\\n• Conversational Email • \\nWe’ve simplified your inbox to help you communicate in real-time with your clients, teams, and friends.\\n \\n• Built-in Team Collaboration • \\nGroups within your inbox - the simplest team collaboration app! No logins, no links, just everyone on the same page.\\n \\n• The more you know • \\nStay on top of urgent messages with read receipts, real-time awareness, and snoozing.\\n \\n \\n• Clear the Clutter • \\nAutomatically organizes your inbox and gets rid of endless email threads, for maximum focus.\\n \\n• The Fastest, Search. Ever. •\\nSearch all messages, files and contacts instantly. Spike’s Super Search is the fastest way to find and attach anything - guaranteed. (Go ahead, test us. And if you’re still not sure, we’ll challenge you to a search-off).\\n \\n• See Through Walls (or files) • \\nVisually preview all attachments without having to open any emails or download any files. one by one.\\n \\n• Bulk actions = saving time • \\nUnsubscribe, mass archive, and organize with unprecedented speed.\\n \\n• Enough with the inbox shuffle •\\nAll of your work and personal email accounts in one simple, unified inbox.\\nSo you can keep it together. Literally. \\nconnect any Gmail, Office365 (e.g Outlook), Exchange, iCloud, Yahoo and any IMAP account. \\n \\n• One life, one Calendar •\\nAll your calendars (Google Calendar, Facebook, Apple Calendar) right in your email, so you can give the switching back and forth a rest. \\n \\n• An arsenal of Expression •\\nWhy use words when you can use gifs, voicenotes, emojis, share location, make voice and video calls, send a drawing, photo or video? And ALL from within your inbox...\\n \\nChat with us at chat@spikenow.com and tell us what you think, or send us a GIF - we love those too!\\nWe’re waiting to hear from you :)\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.pingapp.app\",\n      \"trackName\": \"Spike Email (Formerly Hop)\",\n      \"trackId\": 707452888,\n      \"sellerName\": \"erez pilosof\",\n      \"averageUserRating\": 4.0,\n      \"userRatingCount\": 36\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/c3/93/55/c393551f-9b45-5c67-2178-4db6dec0aaa7/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/94/ce/88/94ce88a4-7f30-4b74-02f8-fcc6f5031f98/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/8a/73/e7/8a73e768-b112-e3cb-f00b-2ea1c2edda29/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/f9/4e/53/f94e53c6-b9ce-43ee-3ae0-f29c55b3a6e4/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/19/67/bc/1967bc9d-4cf2-32f8-d13d-5bd895a03003/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/0d/3e/76/0d3e766c-d481-901b-3ad8-b50fa1b42096/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/0d/3e/76/0d3e766c-d481-901b-3ad8-b50fa1b42096/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/0d/3e/76/0d3e766c-d481-901b-3ad8-b50fa1b42096/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/beijing-kuairu-technology-co-ltd/id1384567075?uo=4\",\n      \"advisories\": [\n        \"Infrequent/Mild Sexual Content and Nudity\"\n      ],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"子弹短信 - 聊天，可以再快一点\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"ZH\"\n      ],\n      \"fileSizeBytes\": \"125533184\",\n      \"sellerUrl\": \"http://www.zidanduanxin.com\",\n      \"contentAdvisoryRating\": \"12+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/%E5%AD%90%E5%BC%B9%E7%9F%AD%E4%BF%A1-%E8%81%8A%E5%A4%A9-%E5%8F%AF%E4%BB%A5%E5%86%8D%E5%BF%AB%E4%B8%80%E7%82%B9/id1384567076?mt=8&uo=4\",\n      \"trackContentRating\": \"12+\",\n      \"currentVersionReleaseDate\": \"2018-11-28T16:54:52Z\",\n      \"releaseNotes\": \"本次更新：\\n- 支持点击淘宝、微博、抖音分享链接跳转至相应的应用\\n- 提升了支付宝付款码的识别率\\n- 支持使用支付宝账户授权登录\\n- 修复了其他 bug 若干，提升了应用稳定性和流畅度\",\n      \"primaryGenreId\": 6005,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6005\",\n        \"6009\"\n      ],\n      \"releaseDate\": \"2018-08-20T07:35:39Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"0.9.7\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1384567075,\n      \"artistName\": \"Beijing Kuairu Technology Co., Ltd.\",\n      \"genres\": [\n        \"Social Networking\",\n        \"News\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"【子弹短信，快如子弹 】\\n 子弹短信是一款高效的聊天软件，语音与文字的完美结合全面提升你的沟通效率，让你的信息随心所达。\\n\\n 【不再繁琐，我想要的极速体验 】\\n * 快捷回复功能：用户无需进入聊天页面，在 App 的消息列表页面就可以快捷回复消息；列表页面支持直接展开多条未读，可以语音或文字快速回复。\\n \\n 【不再鸡肋，我想要的“语音转文字”】\\n 更聪明的“语音转文字”：用户可以自己选择发送信息的类型，可选格式有：语音＋文本、纯语音、纯文本，选择语音＋文字的话，发送语音的同时会自动转为文字并附带，同时语音识别率高达 97%，让用户在不同场景下都有高效的选择；\\n \\n 【 告别忙乱，「稍后处理」让你不再忘事儿】\\n 既然生活和工作分不开家，那不如让效率翻倍，在任何聊天界面，用户都可以设置文字消息为稍后处理项，让聊天不再忙乱。\\n \\n 【 人性化的交互带来效率的更多提升】\\n * 引用回复功能：任何端口都支持引用回复功能，让聊天过程中不再意义不明，拒绝低效率的沟通。\\n * 与非子弹短信用户的好友也可以方便地沟通：用户给手机通讯录中的好友发送子弹短信，将自动调取手机短信权限，即使对方没有下载子弹短信，也可以很方便地回复进行聊天。\\n * 历史头像和“这是谁来着？”：子弹短信的每个用户主页中都将对其好友展示曾经用过的历史头像，好友可以选择将之前任意一张历史头像设置为该用户的当前头像，免除换了头像就不“认识人”的尴尬局面；在子弹短信内点击“这是谁来着”，用户可以看到与该好友前几次的对话记录，帮助用户回想起来这是谁。\",\n      \"minimumOsVersion\": \"9.3\",\n      \"primaryGenreName\": \"Social Networking\",\n      \"bundleId\": \"com.bullet.message\",\n      \"trackName\": \"子弹短信 - 聊天，可以再快一点\",\n      \"trackId\": 1384567076,\n      \"sellerName\": \"Beijing Kuairu Technology Co., Ltd.\",\n      \"averageUserRating\": 2.0,\n      \"userRatingCount\": 35\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/d9/6b/dd/d96bdd1c-2c05-85ce-5568-adaac146dd82/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/d8/3e/f2/d83ef285-d35d-e1ac-2c87-c528929e155e/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/aa/93/2c/aa932cd5-e544-7256-9f2f-a8864f45ae4c/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/90/94/62/9094626f-5ade-cf6a-bf0a-7ae9b721e825/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/3f/97/97/3f979701-925a-bf39-b7f9-37e117fb1491/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/d4/1d/8a/d41d8a9f-54fb-5a18-b130-b10e1601ee10/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/13/ae/b5/13aeb56f-4f61-1b6e-e964-af032f4af0b9/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/46/99/7e/46997efc-6831-38c1-be44-1b31fba04b2e/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/13/05/89/13058926-7cd1-0859-2528-36a7812e8425/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/c9/43/85/c943854d-7554-e9fe-499b-efbff4b05d7f/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/21/04/5a210448-787f-5722-b3b3-d4333bdfdc01/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/37/f1/84/37f184f0-4a21-c692-4524-9f8333bf03b8/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/f8/b4/4a/f8b44ad0-20c4-2093-260a-035254e506c2/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/f8/b4/4a/f8b44ad0-20c4-2093-260a-035254e506c2/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/f8/b4/4a/f8b44ad0-20c4-2093-260a-035254e506c2/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/ringcentral-inc/id293305987?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"RingCentral\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"JA\",\n        \"PT\",\n        \"ZH\",\n        \"ES\",\n        \"ZH\"\n      ],\n      \"fileSizeBytes\": \"415329280\",\n      \"sellerUrl\": \"http://www.ringcentral.com/teams/overview.html\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/ringcentral/id715886894?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2019-01-02T07:16:55Z\",\n      \"releaseNotes\": \"• Quick Contacts – Keep the people you communicate with most close by. RingCentral subscribers can add contacts to the new Quick Contacts carousel in the Contacts tab.\\n• Screen Sharing – Broadcast your entire screen in a RingCentral meeting using Screen Recording in the iOS Control Center. You must have RingCentral Meetings Embedded set as your video service and be using iOS 11 or higher.\\n• Conversation Notification Preferences – Want to get notified of all new messages in a conversation? Or never get notified? Set team or direct message specific notification preferences from the conversation settings screen.\\n• RingCentral Admin Enhancements – Company admins can now access the RingCentral Analytics Portal and RingCentral Call Logs from the RingCentral app.\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2013-10-23T12:02:45Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"5.10.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 293305987,\n      \"artistName\": \"RingCentral, Inc\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"RingCentral is the leading all-in-one voice, team messaging, and video conferencing solution.  Unlock your team’s potential and reduce email clutter with integrated task management, file sharing, and calendaring.\\n \\nHere’s how RingCentral helps teams to get stuff done:\\n \\n• Message real-time one-on-one or with a team—from anywhere and on any device.\\n• Collaborate live with HD video, screen sharing, and group video meetings.\\n• Assign tasks and events to stay productive and accountable.\\n• Share files, photos, links, and notes.\\n• Keep on top of your most important communications with message filters, mention indicators, and new message counts.\\n• Integrate with third-party apps such as Zendesk, Trello, Asana, and JIRA.\\n\\nHave a RingCentral Office subscription? Use the app to take your business number anywhere you go for calls, voice messages, text messages, and faxes.\\n \\nSign in with your RingCentral Office account to do even more:\\n• Make secure phone calls over Wi-Fi without using your carrier minutes.\\n• Show your RingCentral business number as your caller ID when you make calls or send text messages.\\n• Initiate calls directly from a private message.\\n• Make local calls to your home country while traveling internationally over Wi-Fi.\\n• Use the app to keep all your business voicemails and faxes separate from your personal messages.\\n \\nA RingCentral Office subscription is required for certain product features. Features will vary by product and plan. A free subscription is available with limited capabilities.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.glip.mobile\",\n      \"trackName\": \"RingCentral\",\n      \"trackId\": 715886894,\n      \"sellerName\": \"RingCentral, Inc\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 13\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/9b/2b/6a/9b2b6a84-68ec-b49c-a8d3-0a72c6963ae0/source/406x228bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/d4/67/43/d467431b-5517-99ab-fc38-520da1b9ce15/source/406x228bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/18/2f/4e/182f4e14-3fcd-d352-e129-ee8f0b2d410a/source/406x228bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/c6/fc/09/c6fc0929-4968-f317-5c06-cc074af57b38/source/406x228bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/21/6c/61/216c61c2-ff16-d524-12d0-64978ab616cd/source/406x228bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/d7/79/03/d77903be-3b7e-9d6c-169b-20c262946655/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/3b/95/66/3b956633-8690-75a7-02a0-a9b2ba924300/source/552x414bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/cf/33/77/cf3377ce-bce3-7e85-d67c-5e493db1e241/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/51/28/b3/5128b35d-85fb-ce9d-14ff-cc82c3675d1c/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/8d/d8/cd/8dd8cd02-a11a-0b79-08c1-3ddfb5f5d3c2/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/a8/71/b2/a871b214-8792-2e62-0f7a-adaad4d2cd8c/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/a8/71/b2/a871b214-8792-2e62-0f7a-adaad4d2cd8c/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/a8/71/b2/a871b214-8792-2e62-0f7a-adaad4d2cd8c/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/huang-zhiqiang/id556750720?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone3GS-iPhone-3GS\",\n        \"iPhone4-iPhone4\",\n        \"iPodTouchFourthGen-iPodTouchFourthGen\",\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Baby Zoo Hospital\",\n      \"languageCodesISO2A\": [\n        \"CS\",\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"PL\",\n        \"PT\",\n        \"RU\",\n        \"ZH\",\n        \"ES\",\n        \"SV\",\n        \"ZH\",\n        \"TR\"\n      ],\n      \"fileSizeBytes\": \"34650112\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/baby-zoo-hospital/id593413141?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-12-28T20:35:57Z\",\n      \"releaseNotes\": \"Update for iOS 11\",\n      \"primaryGenreId\": 6014,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6014\",\n        \"7003\",\n        \"6017\",\n        \"7015\"\n      ],\n      \"releaseDate\": \"2013-01-21T19:45:43Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.0.1\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 556750720,\n      \"artistName\": \"HUANG ZHIQIANG\",\n      \"genres\": [\n        \"Games\",\n        \"Arcade\",\n        \"Education\",\n        \"Simulation\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Many cubs need care and coziness.You will work in the zoo hospital for cubs. Give them food and play with them. It's important to do everything in time! Your inmates will be the happiest animals in the zoo!\",\n      \"minimumOsVersion\": \"6.0\",\n      \"primaryGenreName\": \"Games\",\n      \"bundleId\": \"com.outsourcingflash.babyZooHospital\",\n      \"trackName\": \"Baby Zoo Hospital\",\n      \"trackId\": 593413141,\n      \"sellerName\": \"HUANG ZHIQIANG\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple115/v4/f3/ff/62/f3ff6252-5a90-1faa-feff-0b373f4eb1ff/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple125/v4/61/08/fa/6108fadd-2164-4250-2c7e-4e993275d7fc/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple125/v4/06/0f/fa/060ffa9d-04a0-087c-a662-58855bed159a/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple115/v4/fc/dd/1a/fcdd1af1-3e89-f6a2-c865-314225ad1e78/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple125/v4/3b/f4/86/3bf48647-fb0e-3d1f-b9f5-c31231c8e334/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple115/v4/d5/73/86/d57386ba-3c10-342a-9f0c-3fb7c8890f9d/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple125/v4/19/75/18/19751853-e934-c273-e849-61cfe4d8c049/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple115/v4/02/6a/4c/026a4c46-0164-3c01-16d3-2bd54b8ce2af/source/576x768bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple115/v4/f0/8c/62/f08c6257-f420-cb94-2734-bb4c039008a5/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple125/v4/31/5f/64/315f647a-0a62-4916-313b-567485102725/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/2f/da/a8/2fdaa883-64d4-7a92-aa54-57f3ca75df6e/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/2f/da/a8/2fdaa883-64d4-7a92-aa54-57f3ca75df6e/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/2f/da/a8/2fdaa883-64d4-7a92-aa54-57f3ca75df6e/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/redbooth-inc/id465462661?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Redbooth\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"78306304\",\n      \"sellerUrl\": \"https://redbooth.com/\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/redbooth/id793346089?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2019-01-02T21:01:04Z\",\n      \"releaseNotes\": \"- See your assignees at a glance on tasks\\n- Misc bug fixes\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2014-01-20T16:30:24Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"8.35.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 465462661,\n      \"artistName\": \"Redbooth, Inc.\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"WHAT IS REDBOOTH\\nRedbooth is an easy to use project management software available for teams to stay organized and get work done. Redbooth allows teams to manage an unlimited number of projects in collaborative workspaces that combine tasks, files and feedback into a centralized, searchable, and in-sync experience; it is the perfect workflow management system! Redbooth teams are more productive because they can easily work together on their favorite device or platform. \\n \\nSTART FAST\\n- Create an account directly through the iOS app\\n- Easily set up dedicated workspaces for each project or task you want to manage\\n- Super intuitive interface for creating and assigning new tasks\\n- Just the right level of functionality for busy teams\\n \\nUPDATE ANYWHERE\\n- View and organize your work from anywhere \\n- Create tasks, conversations or update projects anytime\\n- Add due dates, assignees or comments to any task\\n- Update tasks as work is completed or notify others about changes\\n- Everything is automatically saved and synced\\n \\nTRACK EVERYTHING\\n- See your favorite workspaces and task management lists\\n- Assess the progress of shared projects and spot dependencies early\\n- Visualize progress as you complete projects\\n \\nSTAY CONNECTED\\n- Get notified of important updates\\n- Speed up feedback with integrated messaging tools\\n- Notification settings are fully customizable\\n- Use Redbooth conversations to chat within the app\\n \\nPRICING\\n-Free: 2 users and 2 workspaces for teams getting started with project management\\n-Professional: From $9/mo: subtasks, reporting, and guest users for growing teams\\n-Business: From $15/mo: assignable subtasks and priority support for large teams\\n \\nCOMPARE\\nOther tools like Basecamp, Trello, Wrike, Asana, Aha!, and Microsoft Project can’t come close to the ease of use of Redbooth, which is built specifically for busy teams who don’t have a lot of time to spare.\",\n      \"minimumOsVersion\": \"10.3\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.teambox.Teambox\",\n      \"trackName\": \"Redbooth\",\n      \"trackId\": 793346089,\n      \"sellerName\": \"Redbooth, Inc.\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 56\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/99/e2/05/99e20536-c3c8-6e92-2a01-a6f0f1702c9e/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/4e/49/52/4e4952f8-142b-f288-1d52-4b72592743c4/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/9c/69/94/9c6994ee-7aa0-e21c-4eca-161d4d689773/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/6a/41/0d/6a410da3-cfaf-d9a7-dc21-a439e2dd60a2/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/a5/ba/d1/a5bad1b6-d078-06be-6561-6449a613c96a/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/66/ce/aa/66ceaac0-e550-084f-6e62-576575cb013d/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/66/ce/aa/66ceaac0-e550-084f-6e62-576575cb013d/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/66/ce/aa/66ceaac0-e550-084f-6e62-576575cb013d/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/guy-gubi/id332505557?mt=8&uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"averageUserRatingForCurrentVersion\": 4.5,\n      \"trackCensoredName\": \"Favorites Widget Pro\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"20047872\",\n      \"sellerUrl\": \"http://guygubi.wix.com/favoriteswidget\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 20,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/favorites-widget-pro/id909578530?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-04-27T23:34:02Z\",\n      \"releaseNotes\": \"iMessage is now supported with the free version\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2014-09-18T22:13:38Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"4.1\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 332505557,\n      \"artistName\": \"Guy Gubi\",\n      \"genres\": [\n        \"Productivity\",\n        \"Utilities\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Featured by Lifehacker and Drippler as essential widget!\\n\\nCall and text your favorite contacts directly from the Widgets screen! \\nThis Widget support calling, message, WhatsApp, Slack, Telegram, Facebook Messenger, email and FaceTime.\\n\\nYou can also organize contacts in groups (e.g. Family, Friends, Work) and access them directly from the widget.\\n\\nQuickly order Uber or Lyft to your favorite places from the widget.\\nThe widget show you price comparison between Uber to Lyft so can literally save money!\\n\\nThis is an app with a powerful widget that will dramatically improve everyday use of your iPhone. While currently contacting your friends can be an annoyingly long process because you must open apps and search through contact after contact, with Favorites Widget just swipe right from the lock screen, home screen, or pull down the widgets screen from within any app and get immediate access to your favorites.  \\n\\nSlack support! \\n- Quickly open your channels and Slack contacts from the widget\\n- Multiple teams\\n- Public channels, private channels and direct messages\\n\\nFeatures:\\n● Call & Text from the Notification Center\\n● Groups\\n● Unlimited contacts and groups\\n● 3D Touch in the widget for quick call\\n● Call & Message \\n● WhatsApp\\n● Telegram\\n● Slack\\n● Facebook Messenger\\n● Email\\n● FaceTime & FaceTime Audio\\n● Uber and Lyft integration + price comparison\\n● Full support for iPhone 7 / 7+\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.gubi.Favorites-Widget\",\n      \"trackName\": \"Favorites Widget Pro\",\n      \"trackId\": 909578530,\n      \"sellerName\": \"Guy Gubi\",\n      \"averageUserRating\": 4.5,\n      \"userRatingCount\": 56\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/6e/f9/10/6ef910ca-9e1f-ce53-f277-b08b1ca2e4db/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/58/f8/f7/58f8f79a-a070-34aa-cac2-95fcbf091a2e/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/c4/7c/22/c47c228a-e526-fc3d-f514-c6891c3bbe60/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/f9/7f/e7/f97fe7c8-55e7-ba7c-45a2-a7d143b5c20c/source/552x414bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/af/66/37/af6637be-7f03-eb82-d7bb-a8f91008025e/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/8a/68/46/8a68466c-c8db-deff-a2c5-ae1001636c65/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/ea/d1/08/ead10869-203c-aea7-dcdc-c85830f19d19/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/ea/d1/08/ead10869-203c-aea7-dcdc-c85830f19d19/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/ea/d1/08/ead10869-203c-aea7-dcdc-c85830f19d19/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/slack-technologies-inc/id453420243?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Frontiers by Slack\",\n      \"languageCodesISO2A\": [\n        \"CA\",\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"HE\",\n        \"IT\",\n        \"PL\",\n        \"PT\",\n        \"RO\",\n        \"RU\",\n        \"ZH\",\n        \"ES\",\n        \"ZH\",\n        \"TR\"\n      ],\n      \"fileSizeBytes\": \"126481408\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/frontiers-by-slack/id1433968558?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-09-02T01:59:26Z\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6006\"\n      ],\n      \"releaseDate\": \"2018-09-02T01:59:26Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.1\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 453420243,\n      \"artistName\": \"Slack Technologies, Inc.\",\n      \"genres\": [\n        \"Business\",\n        \"Reference\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Welcome to Frontiers, a conference by Slack! The Frontiers app is your personalized guide. Use it to manage your session schedule, see where the next keynote is happening, and more. After Frontiers, it’ll help you review session materials, or make connections. Whenever (and however) you use it, we’re really looking forward to seeing you soon.\\n\\nMore things you can do:\\n- Explore the conference agenda for all tracks, keynotes and networking events\\n- Navigate the conference: locate sessions, sponsors, gathering places, and more\\n- Email yourself sponsored conference literature\\n- Stay in contact with conference organizers and sponsors\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.slack.frontiers\",\n      \"trackName\": \"Frontiers by Slack\",\n      \"trackId\": 1433968558,\n      \"sellerName\": \"Slack Technologies, Inc.\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple49/v4/9d/ee/29/9dee29f7-2e43-5e51-7aa6-a69db02e2444/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple49/v4/f3/be/01/f3be0111-0283-9426-e15a-62c7d877a4ba/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple49/v4/24/4e/9c/244e9c83-809c-14ee-da9f-203d47a67d4f/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple49/v4/4f/9c/d3/4f9cd3d5-e92c-191d-69e7-5e3c7fd95fe0/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/d9/9b/01/d99b0126-f9d9-e687-e41a-1963b2282991/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple49/v4/8a/df/d3/8adfd3f8-d435-07de-68e3-9d9d7bd5b257/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple49/v4/9b/50/7d/9b507d71-2b41-889b-1fcf-632cc8963a67/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple49/v4/91/d1/c1/91d1c166-9077-d5cd-f0de-a266c4624d8a/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple49/v4/62/cd/fb/62cdfb8e-b44d-95fa-706a-c485b4582985/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/02/bc/6b/02bc6b9f-de0a-fd10-0ca6-6ff0eade505e/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/02/bc/6b/02bc6b9f-de0a-fd10-0ca6-6ff0eade505e/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/02/bc/6b/02bc6b9f-de0a-fd10-0ca6-6ff0eade505e/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/tappforce/id497894754?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"BOARD for JIRA - Scrum Kanban\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"34895872\",\n      \"sellerUrl\": \"http://tappforce.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/board-for-jira-scrum-kanban/id934196108?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-10-06T21:46:26Z\",\n      \"releaseNotes\": \"Bug Fixes\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6000\"\n      ],\n      \"releaseDate\": \"2015-09-02T23:52:09Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.6.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 497894754,\n      \"artistName\": \"Tappforce\",\n      \"genres\": [\n        \"Productivity\",\n        \"Business\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"** Over 1,000 customers ship on time with JIRA Boards **\\n\\nManage your daily standup & scrum teams with JIRA Boards. \\n  \\n- Visualize your team's tickets on a beautiful, fully native (ew, web apps) kanban or agile board (Portrait AND Landscape). Available on both iPhone & iPad. Your first board is always free, forever.\\n\\n- Assign tickets and transition them through your JIRA workflow. Just drag tickets with your finger, it's that easy.\\n\\n- Switch between multiple boards and multiple JIRA accounts\\n\\n- JIRA Board works with JIRA Cloud and JIRA Server.\\n\\n- Automatically login with 1Password!\\n\\n- Perfect for scrum masters, tech leads and managers that practice agile development.\\n\\nALL FEATURES\\n\\n- Scrum and Kanban agile boards supported, including custom workflows\\n- Set assignees, components, and labels\\n- Comment on tickets\\n- Compose new tickets\\n- Rank tickets\\n- Transition tickets\\n- Share tickets to HipChat & Slack\\n- Login with multiple accounts\\n- Login quickly with 1Password\\n- Works with JIRA 6.0 and above\\n- Search for tickets via Spotlight\\n\\nIN APP PURCHASES\\n\\nAll subscriptions unlock unlimited JIRA boards, and come included with a 1 month FREE trial. If you choose to subscribe you will be charged a price according to your country. The price will be shown before you complete the payment. The subscription renews every month or every year depending on your selection, and auto-renews every month or every year unless auto-renew is turned off 24 hours before end of the current subscription period. Your iTunes Account will be automatically charged within 24 hours prior to the end of the current period and you will be charged for one month at a time, or one year at a time depending on your subscription selection. You can turn off auto-renew at anytime from from your iTunes account settings.\\n\\n- Monthly Subscription: Unlock Unlimited Boards for $4.99/mo, automatically renewed monthly until canceled.\\n\\n- One Time Unlimited Purchase: Unlock Unlimited Boards for $99.99 one time only. All boards will be yours forever.\\n\\nPrivacy Policy: http://unbouncepages.com/tappforce-privacy-policy/\\n\\nTerms of Service: http://unbouncepages.com/tappforce-tos/\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.tappforce.JIRAforce\",\n      \"trackName\": \"BOARD for JIRA - Scrum Kanban\",\n      \"trackId\": 934196108,\n      \"sellerName\": \"TAPPFORCE LLC\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/4a/9d/8d/4a9d8da8-db9b-37c4-244b-07f9d495f610/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/49/90/a1/4990a13d-9183-804a-4937-62fb7463d09d/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/0b/26/f8/0b26f813-ca19-a6a4-7ea7-d48f1acb79df/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/aa/c2/aa/aac2aac3-e4bc-5860-0e14-a1a1ea10f2b5/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/ca/ad/b7/caadb7ff-1d93-3754-063a-d355b5611eb9/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/45/82/c1/4582c173-caea-6ad1-0d42-c735e6c7d3db/source/576x768bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/c7/36/cf/c736cfe2-1527-6d47-3c38-d895c39d8c15/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/9b/04/a8/9b04a84f-d2e8-eb8f-350b-cfc68204f6fe/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/9b/04/a8/9b04a84f-d2e8-eb8f-350b-cfc68204f6fe/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/9b/04/a8/9b04a84f-d2e8-eb8f-350b-cfc68204f6fe/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/ca-flowdock/id1224635136?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Flowdock\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"65267712\",\n      \"sellerUrl\": \"http://www.flowdock.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/flowdock/id528568363?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-19T18:51:35Z\",\n      \"releaseNotes\": \"What's New:\\n\\n- Introducing Quick Flows/1-1 Finder! You can swiftly select a flow or 1-1 chat without having to scroll through an extensive list.\\n- Introducing Search! Search all your flow's content, including chat messages, uploaded files, emails and other team inbox items. We recommend it.\\n- Introducing 1-1 Search! Search all your 1-1 chat content including files and links. \\n- You can now view attachments shared in flows and 1-1 chats. \\n\\nBug Fixes:\\n- A sharp uptick in crashes that no longer happen. Because we fixed a handful of them\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6000\"\n      ],\n      \"releaseDate\": \"2012-05-25T15:46:33Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"4.1.4\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1224635136,\n      \"artistName\": \"CA Flowdock\",\n      \"genres\": [\n        \"Productivity\",\n        \"Business\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Official iPhone/iPad app of Flowdock\\n\\nFlowdock is the center of gravity for team communication that helps teams make their regular work a by-product of collaboration through organized chat and a shared integration inbox.\\n \\nIt replaces IM or IRC chat in your team's workflow and frees your mailbox from automated emails. With integrations to over 80+ tools, you’ll always stay up-to-date with what your team is doing.\\n \\nFlowdock allows you to:\\n·         Converse with your teams in Flows and organize your conversations by threads\\n·         Have private conversations with your team mates when needed with 1-1s\\n·         Stay on top of your updates across flows and 1-1s with in app notifications and customize the notifications as needed\\n·         Be expressive with emojis across flows and 1-1s\\n·         Collaborate with your team members by sharing files across flows and 1-1s\\n·         Build your own ongoing knowledge base with the power of hash tags\\n·         Effectively search for content that you need with help of hash tags that you maintain\\n·         View all your integrations in one place with shared integration inbox and converse with your teams on the inbox items to effectively get your work done\\n·         Get attention from only people that matter with @@subgroups\\n \\nFlowdock integrates with your favorite tools, including Trello, Git & GitHub, Pivotal Tracker, Zendesk, Atlassian JIRA, Confluence, Bamboo, Capistrano, Heroku, Redmine, FogBugz, Basecamp, BitBucket, Kiln, Mercurial, Nagios, Pingdom, Hudson / Jenkins and many other project management, issue tracking, wiki, version control, monitoring, deployment & continuous integration systems and services. \\n \\nNOTE: To use the Flowdock app, you need to have a Flowdock account available at: http://www.flowdock.com \\n \\nFor feedback or feature suggestions, check out our Uservoice page at http://flowdock.uservoice.com\\nFor support, reach out to our support team at Team-Flowdock-Support@ca.com\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.flowdock.dalton\",\n      \"trackName\": \"Flowdock\",\n      \"trackId\": 528568363,\n      \"sellerName\": \"CA INC\",\n      \"averageUserRating\": 2.5,\n      \"userRatingCount\": 8\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/fe/4d/b0/fe4db00d-b740-b20f-1d01-c7d8bb3e32d0/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/21/dd/76/21dd7627-2c3a-1df9-d27a-e68db29464df/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/c1/36/38/c13638e4-02a8-6f33-1c97-c6385f47ba60/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/9d/07/6b/9d076b8b-935d-450c-fe46-6eef16683fca/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/db/5b/c6/db5bc65f-a445-2439-8e21-0fa7caf13fc1/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/56/d7/10/56d7104b-716d-f429-edcd-bd9afaa4443c/source/576x768bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/ee/9e/a3/ee9ea373-89b6-7563-fb70-a5894343f27c/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/0e/a3/b4/0ea3b429-fc63-ada6-bf51-746bdbd2d875/source/576x768bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/11/95/4a/11954a96-5307-4d03-114a-4745a3a71a08/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/d9/8e/50/d98e5078-dce5-b684-22f1-0423203e2741/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/d9/8e/50/d98e5078-dce5-b684-22f1-0423203e2741/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/d9/8e/50/d98e5078-dce5-b684-22f1-0423203e2741/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/vector-creations-limited/id1154157774?uo=4\",\n      \"advisories\": [\n        \"Unrestricted Web Access\"\n      ],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 2.5,\n      \"trackCensoredName\": \"Riot.im\",\n      \"languageCodesISO2A\": [\n        \"SQ\",\n        \"EU\",\n        \"BG\",\n        \"CA\",\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"HU\",\n        \"IS\",\n        \"JA\",\n        \"RU\",\n        \"ZH\",\n        \"ES\",\n        \"ZH\",\n        \"VI\"\n      ],\n      \"fileSizeBytes\": \"86716416\",\n      \"sellerUrl\": \"http://riot.im\",\n      \"contentAdvisoryRating\": \"17+\",\n      \"userRatingCountForCurrentVersion\": 2,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/riot-im/id1083446067?mt=8&uo=4\",\n      \"trackContentRating\": \"17+\",\n      \"currentVersionReleaseDate\": \"2018-12-13T09:54:09Z\",\n      \"releaseNotes\": \"This new version supports the consent of matrix servers terms of service (including GDPR) in the registration flow.\\nIt also contains fixes for the \\\"Empty room\\\" bug, the registration issue on iOS 10, etc.\",\n      \"primaryGenreId\": 6005,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6005\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2016-05-05T22:48:51Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"0.7.8\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1154157774,\n      \"artistName\": \"Vector Creations Limited\",\n      \"genres\": [\n        \"Social Networking\",\n        \"Utilities\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Welcome to Riot.im: a new world of open communication!\\n\\nRiot.im is a simple and elegant collaboration environment that gathers your different conversations and app integrations into one single app.\\n\\nBuilt around group chatrooms, Riot.im lets you share messages, images, videos and files - interact with your tools and access all your different communities under one roof.  One single identity and place for all your teams: no need to switch accounts, work and chat with people from different organisations in public or private rooms: from professional projects to school trips, Riot.im will become the center of all your discussions!\\n\\nFeatures include:\\n\\n •  Instantly share messages, images, videos and files of any kind within groups of any size\\n •  See who's reading your messages with read receipts\\n •  Email notifications of missed messages and invites\\n •  Voice and video calling and conferencing \\n •  End-to-end encryption using Olm (https://matrix.org/git/olm)\\n •  Communicate with users anywhere in the Matrix.org ecosystem - not just Riot.im users! Including bridged apps and networks like Slack, IRC and Gitter (more coming soon!)\\n •  Discover and invite users by email address\\n •  Participate in guest-accessible public rooms\\n •  Highly scalable - supports hundreds of rooms and thousands of users\\n •  Fully synchronised message history across multiple devices and browsers\\n •  Finely configurable notification settings, synchronised over all devices\\n •  Infinite searchable chat history\\n •  Interact with bots and integrated third party applications like GitHub, Jira and Jenkins (more to come soon!)\\n •  Permalinks to messages\\n •  Full message search\\n •  Excellent support for all iOS device sizes and orientations\\n\\nFor developers:\\n •  Riot.im is a Matrix client - built on the Matrix.org open standard and ecosystem, providing interoperability with all other Matrix compatible apps, servers and integrations\\n •  Entirely open sourced under the permissive Apache License - get the code from https://github.com/vector-im/riot-ios. Pull requests welcome!\\n •  Trivially extensible via the open Matrix Client-Server API (http://matrix.org/docs/spec)\\n •  Run your own server!  You can use the default matrix.org server or run your own Matrix home server (e.g. http://matrix.org/docs/projects/server/synapse.html)\\n\\nComing soon:\\n •  Add your own integrations, bridges and bots!\\n •  Screen sharing\\n •  Login as multiple users at the same time\\n\\nThe web version of Riot.im is available at https://riot.im/app/\\n\\nRiot.im. Break through.\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Social Networking\",\n      \"bundleId\": \"im.vector.app\",\n      \"trackName\": \"Riot.im\",\n      \"trackId\": 1083446067,\n      \"sellerName\": \"Vector Creations Limited\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple18/v4/eb/a8/1f/eba81fc3-05a4-ede4-abbe-5c334b488635/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple60/v4/05/53/c9/0553c9fe-5f08-a3e7-4b54-708a888b7651/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/d0/eb/a8/d0eba84b-416c-0174-7805-7ccf1188443d/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple60/v4/a1/4d/92/a14d9264-c070-fdeb-90dd-ca87660dc184/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple20/v4/d8/d0/c8/d8d0c85a-008f-034f-3e3b-9913a9b4c234/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple62/v4/9e/18/d0/9e18d0b6-08d8-6a20-65b6-4c2a31848ed5/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple62/v4/9e/18/d0/9e18d0b6-08d8-6a20-65b6-4c2a31848ed5/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple62/v4/9e/18/d0/9e18d0b6-08d8-6a20-65b6-4c2a31848ed5/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/imo-network/id525351458?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone4-iPhone4\",\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"imo班聊-移动办公软件、简单高效的团队沟通平台\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"ZH\"\n      ],\n      \"fileSizeBytes\": \"138503168\",\n      \"sellerUrl\": \"http://www.workchat.com/\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/imo%E7%8F%AD%E8%81%8A-%E7%A7%BB%E5%8A%A8%E5%8A%9E%E5%85%AC%E8%BD%AF%E4%BB%B6-%E7%AE%80%E5%8D%95%E9%AB%98%E6%95%88%E7%9A%84%E5%9B%A2%E9%98%9F%E6%B2%9F%E9%80%9A%E5%B9%B3%E5%8F%B0/id525351455?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2016-09-01T05:51:57Z\",\n      \"releaseNotes\": \"[新增] \\n- 公费电话：免费赠送通话时长，让员工沟通无压力； \\n- 企业用车：员工打车，企业买单，出行无负担； \\n- 新手指南：30秒掌握产品价值点。 \\n\\n[优化] \\n- 语音消息更清晰；\\n- 支持组织架构选人，更符合工作场景； \\n- 工作台性能优化。\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6000\"\n      ],\n      \"releaseDate\": \"2012-06-04T12:32:57Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"7.0.12\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 525351458,\n      \"artistName\": \"imo Network\",\n      \"genres\": [\n        \"Productivity\",\n        \"Business\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"班聊，中国第一个桌面与移动一体化的企业办公沟通协同平台！\\n\\n※※※让班聊为工作点赞※※※\\n\\n【让工作效率飞起来】\\n管理人员，一天90%的时间需要用来沟通工作，可浪费了多少时间在找人？\\n还在一一发邮件？岁月静好，这样浪费，你本来有更多时间可以约会？ \\nBut——有这样一款神器 | imo班聊\\n史上最强的聊天唤醒神器，一秒召集人群开启线上会议；3秒响应紧急审批即刻搞定，什么，手机摇一摇就能打卡？\\n用班聊，高效办公，从此妈妈再也不用担心我因为加班，没有时间约会了！\\n\\n【纯净的办公环境】\\n还在用微信、qq办公？\\n老板、上司、 同事、客户，发朋友圈/说说， 你手抖吗？\\n八卦、搞笑、鸡汤，晒幸福，欲罢不能，怎么有心工作？\\nBut——有这样一款神器 | imo班聊\\n 在班聊，只聊工作的事！纯纯的办公环境，没有最纯，只有更纯，噢耶\\\\(^o^)/\\n更有私有云，给企业独立服务器部署，企业信息都锁在家中，比银行还安全！ \\n\\n【瞬间提高执行力】\\n沟通工作不落地，纯粹就是瞎扯淡，领导敦敦教导+口头任务，早已淹没在聊天记录的深处？\\n一句语音60秒，第50秒有个关键内容没听清楚怎么办？再听一遍50秒？\\nBut——有这样一款神器 | imo班聊\\n一边聊天，一边还能将领导的话直接转成任务，落地执行，语音也能点击暂停。\\n高效&执行，班聊绝不缺一而行！\\n\\n【知识管理】\\n神马？在群里一页一页查找领导说了啥？ \\n想找个以前发过的文件？翻到手断，还是找不到 \\nBut——有这样一款神器 | imo班聊\\n自动将聊天文件分类管理，图片、文件、链接，比你钱包里的money还清楚。\\n\\n【这都是实力】\\n##2010年度中国行业信息化突出贡献奖##\\n##2012中国年度微创新企业100强##\\n##2013上海市高新技术企业##\\n##2104年度中国行业信息化最佳解决方案奖##\\n##2015年度中国大协同联盟副理事长单位##\\n##2015年度上海“五星级诚信创建企业”##\\n\\n【联系我们】\\n官方网站：http://www.workchat.com/\\n官方微博：关注“imo班聊” \\n官方电话：4009206575\\n微信公众号：imoffice_com\\nQQ群：297055286\\n客 服 QQ：3065718494\\n投诉建议:  fankui@workchat.com\",\n      \"minimumOsVersion\": \"7.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.imoffice.i-m-office\",\n      \"trackName\": \"imo班聊-移动办公软件、简单高效的团队沟通平台\",\n      \"trackId\": 525351455,\n      \"sellerName\": \"imo Network\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple69/v4/9b/b1/75/9bb17557-7e57-ac2c-e206-3aec6ea035ee/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple2/v4/aa/bb/11/aabb1141-4199-1a76-4f6e-ba179787e6c0/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple49/v4/62/d6/1a/62d61a61-cbd2-38b1-bc1c-c0b5fa3628ba/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/d5/db/9a/d5db9ae3-eec0-9317-6a07-4c2ece23a382/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple49/v4/64/61/6a/64616a36-20c3-4a08-c7a1-00dd8c58a803/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple49/v4/ff/03/b9/ff03b99d-5d41-a16d-eaf9-4239dbd4936b/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple49/v4/a9/5d/59/a95d59fd-c880-5101-80fd-c5cb22cf462e/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple82/v4/0c/c2/41/0cc241af-c1d7-c2e3-5c56-2b8ca7540d8d/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple82/v4/0c/c2/41/0cc241af-c1d7-c2e3-5c56-2b8ca7540d8d/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple82/v4/0c/c2/41/0cc241af-c1d7-c2e3-5c56-2b8ca7540d8d/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/tappforce/id497894754?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Tappsana for Asana - Offline Team Collaboration\",\n      \"languageCodesISO2A\": [\n        \"CA\",\n        \"CS\",\n        \"DA\",\n        \"NL\",\n        \"EN\",\n        \"FI\",\n        \"FR\",\n        \"DE\",\n        \"EL\",\n        \"HE\",\n        \"HU\",\n        \"ID\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"NB\",\n        \"PL\",\n        \"PT\",\n        \"RO\",\n        \"RU\",\n        \"ZH\",\n        \"SK\",\n        \"ES\",\n        \"SV\",\n        \"ZH\",\n        \"TR\"\n      ],\n      \"fileSizeBytes\": \"18473984\",\n      \"sellerUrl\": \"http://tappforce.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/tappsana-for-asana-offline-team-collaboration/id684973303?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2016-12-31T23:09:36Z\",\n      \"releaseNotes\": \"Bug Fixes\\nEnhancements\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6000\"\n      ],\n      \"releaseDate\": \"2013-09-19T03:28:38Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.1.2\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 497894754,\n      \"artistName\": \"Tappforce\",\n      \"genres\": [\n        \"Productivity\",\n        \"Business\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Tappsana is a full featured Asana client for the iPad, iPhone and iPod Touch that works offline and faster than ever.\\n\\nView all of your Asana projects and assign tasks to your team members, whether you’re online, in an airplane or running late at the subway. \\n\\nKEY FEATURES\\n\\nOFFLINE MODE\\n\\n- Create projects and tasks while you’re offline. The next time you’re back online, everything syncs with Asana automatically.\\n\\nENHANCED FOR NEW DEVICES\\n\\n- Support for iPhone 6S and iPhone 6S Plus\\n\\nENHANCED FOR IPAD\\n\\n- Triple pane UI lets you get your work done quickly. Even on iPad Pro\\n\\nOTHER ASANA FEATURES\\n\\n- Quickly add Sub Tasks\\n- Sort by Assignee, Due Date or Manually,\\n- Tagging Support\",\n      \"minimumOsVersion\": \"8.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.tappforce.Tappsana\",\n      \"trackName\": \"Tappsana for Asana - Offline Team Collaboration\",\n      \"trackId\": 684973303,\n      \"sellerName\": \"TAPPFORCE LLC\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple117/v4/11/33/08/113308af-09f0-5c8c-815e-6e96f53fcef1/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple127/v4/7a/3d/5d/7a3d5d77-bfc1-08b5-8864-cbd7d36c6c7a/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple127/v4/3a/eb/ee/3aebeeb3-7eef-97ab-3ad7-caabbbd778b1/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple117/v4/8d/f0/1e/8df01e91-57a0-41ab-3c33-857ac924e242/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple127/v4/fc/6f/f6/fc6ff668-5ceb-78b2-e66e-cba2a2ea66f2/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple117/v4/58/9a/62/589a6225-5c46-e568-c6b7-0a042f10ae40/source/552x414bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple117/v4/d7/79/3f/d7793fb6-7176-53eb-d716-97693cee488c/source/552x414bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple127/v4/1c/7f/12/1c7f12c8-7d98-0ece-209a-1b834df08a76/source/552x414bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple117/v4/9a/8e/50/9a8e50fa-a8f0-006c-3ee6-2fc4cd5a6a75/source/552x414bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple117/v4/b1/11/40/b1114098-cad8-cf47-7fbc-b97db2e16796/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/e6/de/c5/e6dec595-5cf7-8e40-5f35-8ca949c04a9e/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/e6/de/c5/e6dec595-5cf7-8e40-5f35-8ca949c04a9e/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/e6/de/c5/e6dec595-5cf7-8e40-5f35-8ca949c04a9e/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/simply-good-software/id385251756?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Pyrus\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"RU\"\n      ],\n      \"fileSizeBytes\": \"91484160\",\n      \"sellerUrl\": \"http://pyrus.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/pyrus/id385251753?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-29T12:38:45Z\",\n      \"releaseNotes\": \"Fixed errors that some users encountered when filling out forms.\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2010-08-07T04:45:48Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"4.152\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 385251756,\n      \"artistName\": \"Simply Good Software\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Pyrus is the team communication tool for your entire business. It incorporates real-time messaging, task delegation, and approval flows. Finally, all your tasks and conversations are in one easy-to-use interface. \\n\\n- Communicate: every task is a message thread working towards a specific goal.\\n- Delegate: every task has a single person responsible for it at any given moment.\\n- Organize: hide a task from your inbox when you don’t need to act, or snooze it when you want a friendly reminder later. \\n- Track: a powerful search capability makes it easy to find the details you need.\\n- Anywhere: Pyrus syncs seamlessly across all your devices and even works offline.\\n\\nFEATURE HIGHLIGHTS\\n- Swipe right to hide a task from your inbox and stand by, swipe left to archive it and mark it as complete.\\n- Forward any email to x@pyrus.com to turn it into a task.\\n- Attach documents, photos, or files from cloud storage like Box, Dropbox, and Google Drive.\\n- Use subtasks to split a large task into a list of action items.\\n- Get a notification whenever your action or input is requested.\\n- Share images and documents from other apps.\\n\\nPyrus is free to use for an unlimited number of people, offering upgradeable plans for extended usage, increased storage, unlimited API calls, and custom data access policies.\\n - Login with your G+ account \\n- Invite colleagues from Address Book \\n- Work with outsourcers and subcontractors. \\n\\n*** Notifications \\n\\n- Badge on app icon shows the number of unread tasks in your Inbox \\n- You receive push notification when something important happens with your tasks in Pyrus\\n\\nPyrus works offline and seamlessly syncs in background. No frozen UI, no duplicates.\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"net.papirus.iphoneclient\",\n      \"trackName\": \"Pyrus\",\n      \"trackId\": 385251753,\n      \"sellerName\": \"Simply Good Software, Inc\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/72/19/99/7219991b-a71f-4be7-ee95-5dd9fd5e3e23/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/b4/f4/02/b4f402c6-bb6b-cdc7-9ce0-ee8e355433ee/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/9a/9e/d8/9a9ed891-12fd-10be-b81a-3370bc3e9c2c/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/af/d9/51/afd95186-f943-014f-b364-8cb55d9730c6/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/26/82/71/26827115-57a9-d2ba-66a3-b710766f0e42/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/26/2f/de/262fde6d-184a-3ca5-8473-0347c5186476/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/4d/62/a4/4d62a483-dd08-085d-901e-2c84f947d3b7/source/552x414bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/be/b6/01/beb6019a-cc4f-3351-5b81-2dbbce183f35/source/552x414bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/20/80/5f/20805f6d-4033-9cf2-b89e-e9b8b1565f84/source/552x414bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/48/d8/6d/48d86ded-123e-0e5f-47ec-95fd69c19398/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/83/a6/e8/83a6e845-cf7b-4498-16bd-6a502b6d61fa/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/bb/cb/ad/bbcbadbc-85ba-14d5-2daa-928fee0af97b/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/bb/cb/ad/bbcbadbc-85ba-14d5-2daa-928fee0af97b/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/bb/cb/ad/bbcbadbc-85ba-14d5-2daa-928fee0af97b/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/rocket-chat-technologies-corp/id1148477217?uo=4\",\n      \"advisories\": [\n        \"Infrequent/Mild Mature/Suggestive Themes\"\n      ],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"Rocket.Chat\",\n      \"languageCodesISO2A\": [\n        \"CS\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"EL\",\n        \"IT\",\n        \"JA\",\n        \"PL\",\n        \"PT\",\n        \"RU\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"32531456\",\n      \"sellerUrl\": \"https://rocket.chat\",\n      \"contentAdvisoryRating\": \"9+\",\n      \"userRatingCountForCurrentVersion\": 2,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/rocket-chat/id1148741252?mt=8&uo=4\",\n      \"trackContentRating\": \"9+\",\n      \"currentVersionReleaseDate\": \"2018-12-17T08:39:45Z\",\n      \"releaseNotes\": \"- Italian language support;\\n- All message components were rewritten, with a much better UI and reading experience;\\n- Dynamic Type support on the messages list;\\n- Lots of performance improvements on the messages list;\\n- Many bug fixes;\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6005\"\n      ],\n      \"releaseDate\": \"2017-03-08T22:45:04Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.2.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1148477217,\n      \"artistName\": \"Rocket.Chat Technologies Corp.\",\n      \"genres\": [\n        \"Business\",\n        \"Social Networking\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Rocket.Chat is a free and open source team chat collaboration platform that allows users to communicate securely in real-time across devices on web, desktop or mobile and to customize their interface with a range of plugins, themes and integrations with other key software. \\n\\nBy opting for Rocket.Chat, users also benefit from free audio and video conferencing, guest access, screen and file sharing, LiveChat, LDAP Group Sync, two-factor authentication (2FA), E2E encryption, SSO, dozens of OAuth providers and unlimited users, guests, channels, messages, searches and files. Users can set up Rocket.Chat on cloud or by hosting their own servers on-premises.\\n\\nWith more than 700 developer-contributors and over 17k stars on Github, Rocket.Chat has the largest and most active community of chat developers in the open source communication sector.\\n\\nWhen you choose Rocket.Chat, you join a passionate community who help to grow the platform with us!\\n\\nKEY FEATURES:\\n\\n* Free Open Source Software\\n* Hassle free MIT license\\n* BYOS (bring your own server)\\n* Multiple Rooms\\n* Direct Messages\\n* Private Groups\\n* Public Channels\\n* Desktop and Mobile Notifications\\n* Edit and Delete Sent Messages\\n* Mentions\\n* Avatars\\n* Markdown\\n* Emojis\\n* Choose between 3 themes: Light, Dark, Black \\n* Sort conversations alphabetically or group by activity, unread or favourites\\n* Transcripts / History\\n* File Upload / Sharing\\n* I18n - [Internationalization with Lingohub]\\n* Hubot Friendly - [Hubot Integration Project]\\n* Media Embeds\\n* Link Previews\\n* LDAP Authentication\\n* REST-full APIs\\n* Remote Locations Video Monitoring\\n* Native Cross-Platform Desktop Application\\n\\nNEWS:\\n\\nFeatured on: Hacker News, Wired, Product Hunt, JavaScript Weekly, WWWhatsNew, ClasesDePeriodismo\\n\\nGET IT NOW:\\n\\n* Learn more and install: https://rocket.chat\\n* ONE-CLICK-DEPLOYMENT – See instructions on our GitHub repository: https://github.com/RocketChat\",\n      \"minimumOsVersion\": \"11.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"chat.rocket.ios\",\n      \"trackName\": \"Rocket.Chat\",\n      \"trackId\": 1148741252,\n      \"sellerName\": \"Rocket.Chat Technologies Corp.\",\n      \"averageUserRating\": 3.5,\n      \"userRatingCount\": 40\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/04/8f/b3/048fb35b-eeab-b98b-11cc-c6e16e2c5254/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/52/cc/38/52cc38d2-5d87-f1c8-ea5e-ddfb4624f6d1/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple62/v4/8a/f3/4c/8af34cf6-dd73-dcb1-13cb-483822d5eb93/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple71/v4/b1/ba/c6/b1bac625-3b6e-fb5b-28ea-98025e612074/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple62/v4/49/c3/b6/49c3b6c4-570d-38dd-aa1f-d265c8abdf12/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/79/82/6c/79826cd6-c70e-a19e-871a-b348d3209c01/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/79/82/6c/79826cd6-c70e-a19e-871a-b348d3209c01/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/79/82/6c/79826cd6-c70e-a19e-871a-b348d3209c01/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/truelancer/id1142111620?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"Search Jobs & Hire Freelancer\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"26347520\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 2,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/search-jobs-hire-freelancer/id1142111951?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-09-20T00:40:33Z\",\n      \"releaseNotes\": \"- New Phone Verification .\\n- Bug Fixes.\\n- Performance Enhancement\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2016-10-04T17:01:13Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"5.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1142111620,\n      \"artistName\": \"Truelancer\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Truelancer Mobile App is one of the Best Freelance App to Hire Top Freelancers.\\nTruelancer is a good Job Search App to start your Career. \\nYou can Search Jobs & Hire Freelancers for work.\\n\\nStart Earning doing Online Jobs & get Online work from home Jobs using Truelancer Mobile App.\\n\\nHire Developers, Designers, Virtual Assistants and get work. \\n\\nStart Earning by doing Part-time Jobs from Home. \\n\\nBecome a Freelancer & Get Online Freelance Jobs\\n\\nGet the freelancer app on Iphone, this is one of the trending freelance apps. \\n\\nUsing Truelancer app you can get curated gigs like fiverr app.\\n\\n\\nLooking for Jobs?\\n- Create a Free Account & Complete your Profile.\\n- Search Jobs / Freelance Projects and Apply.\\n- Get Freelance Jobs & Earn.\\n- Payment Security using Safe Deposit.\\n- List Services & Sell 24x7\\n\\nGet new Freelance Work Opportunity\\n\\nLooking to Hire?\\n- Post a Project, Its Free!\\n- Buy great value professional Services.\\n- Find professional Freelancers.\\n- Pay only once Satisfied.\\n- Shortlist and Curate your virtual workforce.\\n- Have a Creative Design Project? Post a Contest (Logo Design, Business Card Design, Banner Design etc)\\n- Buy Gigs\\n\\nFacing Issue or Want to share feedback - write us at mobile@truelancer.com\\n\\nOutsource work and save money.\\n\\nHere you can use free resume builder to make a free resume. You have best resume builder app when you use Truelancer app.\\n\\nEmployers can transfer money to freelancers and use Truelancer to send money online.\\n\\nUse Search Jobs & Hire Freelancer Mobile App by Truelancer for free for as long as you want. Upgrade to a paid plan for added benefits like increased earning, more jobs, unlimited skills and verified profile.\\n\\nStudents and Freshers can find Online Jobs and many part time jobs and earn money online.\\n\\nLinkedIn Recruiter can Hire Talented & Skilled Freelancers here on Truelancer Mobile App.\\n\\nFreelancers can Earn Money and withdraw it using TransferWise, Paypal, Payoneer, Paytm, Skrill, Payza, Bank Account and xoom.\\n\\nYou can use Udacity to Learn Programming, Coursera to learn online courses and Truelancer to find work and earn money\\n\\nIf you are using Slack, Trello, Asana, HipChat, Prezi to manage your Teams you should use Truelancer to Hire & work with Freelancers and Remote Work Force.\\n\\n\\nTruelancer ® , Truelancer.com ® are Copyright © of Truelancer\",\n      \"minimumOsVersion\": \"8.1\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"com.truelancer.app\",\n      \"trackName\": \"Search Jobs & Hire Freelancer\",\n      \"trackId\": 1142111951,\n      \"sellerName\": \"TRUELANCER INTERNET PRIVATE LIMITED\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple22/v4/34/a7/4b/34a74bbe-8eb6-b6a8-5f0b-2b04dd20a57e/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple62/v4/ff/1d/5c/ff1d5c13-fdc6-7e05-0981-78068652f972/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple41/v4/31/5f/bf/315fbf27-de45-f083-523b-b186330a5147/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple22/v4/22/14/6e/22146e78-db33-39b6-2eee-18c2bb5490b6/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple62/v4/0c/8a/76/0c8a7672-604d-bd9a-718c-c7d169076dfe/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/0d/84/46/0d844666-8329-0c5a-fdbf-44188054c4a3/source/576x768bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple125/v4/4d/0f/c4/4d0fc4cb-79ba-e894-c4ad-00ecb3efd08c/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple125/v4/4d/0f/c4/4d0fc4cb-79ba-e894-c4ad-00ecb3efd08c/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple125/v4/4d/0f/c4/4d0fc4cb-79ba-e894-c4ad-00ecb3efd08c/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/mubiquo/id353965340?uo=4\",\n      \"advisories\": [\n        \"Unrestricted Web Access\"\n      ],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Loopy Messenger - Professional\",\n      \"languageCodesISO2A\": [\n        \"AR\",\n        \"CA\",\n        \"ZH\",\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"HI\",\n        \"HU\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"PT\",\n        \"RU\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"145562624\",\n      \"sellerUrl\": \"http://loopy.cc\",\n      \"contentAdvisoryRating\": \"17+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/loopy-messenger-professional/id860456941?mt=8&uo=4\",\n      \"trackContentRating\": \"17+\",\n      \"currentVersionReleaseDate\": \"2018-07-11T10:35:56Z\",\n      \"releaseNotes\": \"- Loopy Store. Single section with all the Loopy features you can buy under Preferences menu.\\n- Major UI/UX bugfixing. Fixed issues related to Stickers and Gestures.\",\n      \"primaryGenreId\": 6005,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6005\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2014-04-24T14:48:09Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"6.1\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 353965340,\n      \"artistName\": \"MUBIQUO\",\n      \"genres\": [\n        \"Social Networking\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"With +2M downloads Loopy is the most downloaded Telegram client on iPhone after Telegram. Loopy's mission is to create the best possible Telegram experience for professional communications.\\n\\nOver the superfast, multi-device and secure Telegram protocol for text messages and _Calls_ Loopy empowers teams and users with true mobile real-time collaboration.\\n\\nTeams text information that normally becomes ephemeral. In contrast to standard messengers, Loopy enables teams to make documents out of any conversation converting information into corporate knowledge.\\n\\nShare anything instantly, notes, to-dos, tasks, goals, documents, video and audio messages, open compressed documents (.ZIP, .RAR,...), even you can check email coming from your chat group members without quiting the app !\\n\\nSafe time with color tag chats and set secure fingerprint access among other security features like secret chats.\\n\\nLoopy has been the first messenger to provide:\\n\\n- Email client\\n- .ZIP & .RAR support\\n- Face/Fingerprint ID security\\n- Audio Player (any audio format like ogg, flac, opus...)\\n- One-Tap-Front-Cam Video Messages\\n- Realtime Collaboration Lists\\n- Realtime Collaboration Memos\\n- File Sharing Container\\n- iCloud Drive\\n- Color Tags\\n- Ribbon based access to Documents and Media views for top ease of use.\\n\\nIMPORTANT, IF YOU USE TELEGRAM, YOU DON'T HAVE TO RE-INVITE ANYBODY. YOU WILL FIND YOUR TELEGRAM CONTACTS AND CHATS IN Loopy !!\\n\\nGroup chats with up to 10000 members, sharing videos and documents of any type, send multiple photos from the web, and forward any media you receive in an instant. All your messages are in the cloud, so you can easily access them from ***any of your devices***.\\n\\nFor those interested in maximum privacy, Face/Fingerprint ID and Secret Chats are supported, featuring end-to-end encryption to ensure that a message can only be read by its intended recipient. When it comes to Secret Chats, nothing is logged on Telegram servers and you can automatically program the messages to self-destruct from both devices so there is never any record of it. \\n\\n*** Why Switch to Loopy ? *** \\n\\nYou get all the advantages of the official Telegram client + Value Added functionality that lets you create content from conversations (Memos, Lists...), open .ZIP or .RAR compressed files, reproduce formats like Ogg Vorbis, Flac or Opus among many other features. \\n\\nTelegram protocol is: \\n\\nFAST: The fastest messaging system on the market because it uses a distributed infrastructure with data centers positioned around the globe to connect users to the closest possible server. \\n\\nSECURE: It is Telegram mission to provide the best security among mass messengers. Telegram is based on the MTProto protocol that is built upon time-tested algorithms to make security compatible with high speed delivery and reliability on weak connections.\\n\\nCLOUD STORAGE: Seamlessly sync across all your devices, so you can always securely access your data. Your message history is stored for free in the Telegram cloud. Never lose your data again!\\n\\nGROUP CHAT & SHARING: You can form large group chats and broadcast lists of up to 10000 members, quickly share large videos, documents (.doc, .ppt, .zip, etc.), and send an unlimited amount of photos to your friends. \\n\\nRELIABLE: Built to deliver your messages in the minimum bytes possible, this is the most reliable messaging system ever made. It works even on the weakest mobile connections. \\n\\nPRIVACY: Telegram takes your privacy very seriously and will never give third parties access to your data!\\n___\\nLoopy Messenger might contain ads in your chatlist that you can deactivate.\",\n      \"minimumOsVersion\": \"9.3.5\",\n      \"primaryGenreName\": \"Social Networking\",\n      \"bundleId\": \"com.mubiquo.telegram\",\n      \"trackName\": \"Loopy Messenger - Professional\",\n      \"trackId\": 860456941,\n      \"sellerName\": \"MUBIQUO APPS SL\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/cb/9e/0d/cb9e0d26-9962-bb9b-51b6-e39988c78690/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/fd/4f/46/fd4f46e3-e974-429d-885d-89be815145d3/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/a0/0a/16/a00a16c1-96ea-76c7-0fb6-05c3bb9b18fe/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/98/7f/a0/987fa0d6-7927-25d8-663d-f19c5a995820/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/c3/e4/0a/c3e40a73-91ec-5ed7-34e9-d8dc6452ed3e/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/56/88/70/568870f6-b33f-17cc-1f5e-de71db0d9c3f/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/56/88/70/568870f6-b33f-17cc-1f5e-de71db0d9c3f/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/56/88/70/568870f6-b33f-17cc-1f5e-de71db0d9c3f/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/storymatik-software-unipessoal-lda/id891398405?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"Storyo: Pic jointer for videos\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"PT\",\n        \"RU\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"139730944\",\n      \"sellerUrl\": \"http://storyoapp.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/storyo-pic-jointer-for-videos/id891398402?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-12-06T01:03:18Z\",\n      \"releaseNotes\": \"2.2 version is here and contains a bunch of new features.\\n\\nWe heard your feedback, and Storyo 2.2 brings new themes, Google Photos integration, and more editing options.\\nWe also fixed bugs and improved the loading speed and automatic selection.\\n\\nWe hope that Storyo helps you share video memories of the best moments of your life.\\n\\nNew:\\n+ Two new styles to choose from: \\\"Book\\\" and \\\"Takes\\\" are the brand new styles available to create unforgettable memories. Both styles require iOS11 and \\\"Takes\\\" is only available to iPhone 6s or newer models users.\\n+ Google Photos integration: You can now connect your Google Photos account to add your backed up photos to Storyo.\\n+ Edit options: Now you can add Title slides to better tell your story. We have also added fine-tuning tools that help you reposition photos and easily add more photos to your story. Facebook and Shutterstock resources are now easier to add.\",\n      \"primaryGenreId\": 6008,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6008\",\n        \"6016\"\n      ],\n      \"releaseDate\": \"2014-07-03T09:15:14Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"2.2.4\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 891398405,\n      \"artistName\": \"Storymatik Software, Unipessoal, Lda\",\n      \"genres\": [\n        \"Photo & Video\",\n        \"Entertainment\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"An amazing visual storyteller for documenting unforgettable moments.\\n \\nCreate memorable group videos in seconds by inviting friends and family to share their photos through Storyo and watch the magic unfold.\\n \\nCongrats 5*\\n“Super easy for birthdays and weddings, to create recap films to share on social media, LIKE!! Congrats, keep up”\\n \\nAwesome 5*\\n“This Animoto type of app is amazing. This app turns your photos and memories into greatest journals of your life. easy and fast to create videos just we have to select a timeframe.so simply it is fun and worth the download”\\n \\nFeatured on The Next Web: “Accurate and meaningful video stories made in seconds.”\\nFeatured on iPhone Ticker: “Better than Apple “Memories”\\nFeatured on Apps400: “The app is amazingly wonderful since it turns photos into fantastic simple videos which can be edited by the user as they please.”\\n \\nStoryo gathers the best photos from everyone who shared in the fun, unlocks amazing details, and pulls together related Facebook posts and facts to create a compelling group video memory – ready for everyone to edit or enjoy as is.\\n \\nIf you like sharing video collages, travel videos, and journals, you’ll love telling the bigger story with Storyo.\\n \\nHere’s why:\\n \\n--- IT’S EVEN EASIER TO TURN PHOTOS INTO VIDEO MEMORIES ---\\n \\nCREATE your own video memory with 3 simple taps. Just select the photos you like from start to finish and press PLAY.  Storyo turns your favorite memories into videos, automatically, from your collection of photos.\\n \\nChoose between your most cherished memories - a friends’ night out, a two-week vacation or even an entire year - and leave the rest to Storyo: In just a few seconds, you’ll have a beautiful ready-made Storyo with titles, captions, and maps to bring them come to life.\\n \\nIf you are not completely happy with the result you can now fully edit your story by adding more photos, maps, facebook posts and many others.\\n \\n--- GROUP VIDEO MEMORIES ---\\n \\nHad an amazing time with friends and now thousands of photos dumped on that Whatsapp group? Only Storyo gives you the ability to create unique group video memories with friends and family in an easy way.\\n \\nBy joining photos and all perspectives from everyone that shared one moment, Storyo helps you deliver a better story of your life most magical moments.\\n \\n--- HOW GROUP STORYTELLING WORKS ---\\n \\nEasily invite your friends to be a part of your story.\\n \\nOnce they accept, Storyo gathers everyone’s best photos from that really great time and incorporates them into your video memory.\\n \\nThe more perspectives you include, the easier it is to recapture the best of any experience.\\n \\n--- DISCOVER NEW STORIES IN YOUR CAMERA ROLL ---\\n \\nSome of our best moments have been long forgotten on our Camera Roll. Based on photo timelines, Storyo analyses your camera roll to find the stories behind your photos.\\n \\nIt recognizes that amazing trip, a holiday gathering, a fantastic evening, or the old photos of your pet.\\n \\nSo every time you open Storyo, you’ll find a collection of suggested memories under the section ‘Timeline’.\\n \\n--- FACEBOOK POSTS AND WEATHER INFO ---\\n \\nLog in with Facebook and Storyo will mix in posts that best express the emotions of that day. And to remind you how hot, cold or perfect it was, Storyo can include the weather.\\n \\n--- EXCLUSIVE TO 2.0 - SHUTTERSTOCK VIDEOS ---\\n \\nAdd a professional touch to your stories with stunning videos from a specially curated collection by Shutterstock.\\nStoryo 2.0 automatically suggests video clips that seem best suited. Pick the one you like best and the next thing you know, it’s a part of your video.\\n \\n------\\nHave feedback?\\n \\nConnect with us:\\n> E-mail: hello@storyoapp.com\\n> Facebook: https://www.facebook.com/storyoapp\\n> Twitter: https://twitter.com/storyoapp\\n> Website: http://storyoapp.com/\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Photo & Video\",\n      \"bundleId\": \"com.StoryMatik.Storyo\",\n      \"trackName\": \"Storyo: Pic jointer for videos\",\n      \"trackId\": 891398402,\n      \"sellerName\": \"STORYMATIK SOFTWARE, UNIPESSOAL, LDA\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple111/v4/7f/7a/f3/7f7af305-5939-c073-17e1-eb9d840719d8/source/406x228bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/6b/09/4c/6b094cc5-39f5-d378-1422-8cab7a8a5e6e/source/406x228bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple111/v4/9e/38/92/9e389208-1588-bc8e-642b-29b8196139c3/source/406x228bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple82/v4/f2/5b/09/f25b0934-ebac-f3e5-35f1-7a41229bdcbd/source/406x228bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple111/v4/02/39/38/02393865-4b44-2593-f1ff-5eb72d186dc7/source/406x228bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple122/v4/8a/35/ff/8a35ff54-df04-addf-ff8c-ba394a24b202/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/0a/35/c1/0a35c1e5-2f84-7251-19f8-e9249eb32b22/source/552x414bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple122/v4/01/ba/28/01ba28a5-ede8-44ee-514c-2d2e5b7f9bf6/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/98/57/f4/9857f412-7564-913f-2e7d-fcea148876f0/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple122/v4/d1/45/6e/d1456e8b-0465-eaec-5541-66f0210db3e1/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/1b/99/3a/1b993ab0-2f82-ef23-3c3f-9a4cdad9796e/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/1b/99/3a/1b993ab0-2f82-ef23-3c3f-9a4cdad9796e/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/1b/99/3a/1b993ab0-2f82-ef23-3c3f-9a4cdad9796e/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/blair-barnes/id1130836011?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone3GS-iPhone-3GS\",\n        \"iPhone4-iPhone4\",\n        \"iPodTouchFourthGen-iPodTouchFourthGen\",\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"Hawaii Shopaholic —Shopping, Dress Up & Makeover\",\n      \"languageCodesISO2A\": [\n        \"CS\",\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"PL\",\n        \"PT\",\n        \"RU\",\n        \"ZH\",\n        \"ES\",\n        \"SV\",\n        \"ZH\",\n        \"TR\"\n      ],\n      \"fileSizeBytes\": \"47202304\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 1,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/hawaii-shopaholic-shopping-dress-up-makeover/id1143396573?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-01-04T08:43:23Z\",\n      \"releaseNotes\": \"Performance improved.\",\n      \"primaryGenreId\": 6014,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6014\",\n        \"7014\",\n        \"7015\",\n        \"6016\"\n      ],\n      \"releaseDate\": \"2016-09-07T14:05:00Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.0.3\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1130836011,\n      \"artistName\": \"Blair Barnes\",\n      \"genres\": [\n        \"Games\",\n        \"Role-Playing\",\n        \"Simulation\",\n        \"Entertainment\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Stroll along a beautiful Hawaii beach and browse the shops for clothes, hairstyles and accessories! You have a limited budget but don't worry, if you run out of money you can always earn more!\\nBrowse these bodacious beach-side boutiques! You'll be sayin' \\\"Aloha\\\" in no time!\",\n      \"minimumOsVersion\": \"6.0\",\n      \"primaryGenreName\": \"Games\",\n      \"bundleId\": \"424.shopaholichawaii\",\n      \"trackName\": \"Hawaii Shopaholic —Shopping, Dress Up & Makeover\",\n      \"trackId\": 1143396573,\n      \"sellerName\": \"Blair Barnes\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple3/v4/2e/bf/e3/2ebfe32d-b0dc-52fa-1039-760142245a9c/source/640x1136bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/4f/d0/4f/4fd04fe5-818d-66c1-b57c-45767a8ff9de/source/640x1136bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/6c/c3/71/6cc37155-daaa-19bc-761f-07e857aa01f1/source/640x1136bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/39/6f/c1/396fc1e1-0e93-11ad-5cb1-69049b06722f/source/640x1136bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple1/v4/e2/5c/15/e25c1548-4dfe-721c-97a9-2be0658d8111/source/640x1136bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/62/54/3c/62543cf6-ca64-b346-9a5d-2ddb846f703a/source/360x480bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple3/v4/71/b7/0c/71b70cb7-93ea-f6b9-46ea-7c80cddc13b1/source/360x480bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/d5/af/44/d5af44f1-8ce6-f6dc-5cbd-81625a3467d0/source/360x480bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/f0/80/e7/f080e7ee-bdf9-5566-f359-86ee9a1771e1/source/360x480bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple3/v4/37/3c/c8/373cc802-c1ff-0c48-75e0-688a35e06b3f/source/360x480bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/c8/5d/51/c85d5131-527a-80c9-3a16-793d5cdd74c5/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/c8/5d/51/c85d5131-527a-80c9-3a16-793d5cdd74c5/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/c8/5d/51/c85d5131-527a-80c9-3a16-793d5cdd74c5/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/wu-xiaopeng/id918018655?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone3GS-iPhone-3GS\",\n        \"iPadWifi-iPadWifi\",\n        \"iPad3G-iPad3G\",\n        \"iPodTouchThirdGen-iPodTouchThirdGen\",\n        \"iPhone4-iPhone4\",\n        \"iPodTouchFourthGen-iPodTouchFourthGen\",\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 2.5,\n      \"trackCensoredName\": \"Animated Gif Camera Lite - Whats Funny yik Animated Gifs Creator App\",\n      \"languageCodesISO2A\": [\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"ZH\",\n        \"ES\",\n        \"ZH\"\n      ],\n      \"fileSizeBytes\": \"6000640\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 3,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/animated-gif-camera-lite-whats-funny-yik-animated-gifs/id960663264?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2015-05-11T23:18:28Z\",\n      \"releaseNotes\": \"- Bug Fix\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6007\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2015-03-06T08:32:49Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"2.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 918018655,\n      \"artistName\": \"Wu Xiaopeng\",\n      \"genres\": [\n        \"Productivity\",\n        \"Utilities\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"You will love GifMaker !!\\n\\nCreate beautiful animated gif photos from videos or photo album with the app and instantly share to the world, your friends or just yourself. \\n\\n\\n********* Features *********\\n- Make GIFs from videos\\n- Make GIFs from photos in albums\\n- Adjust speed of the gif photo\\n- Save GIFs automated\\n\\nIf you are looking for a great GIF making tool, check out our \\\"GifMaker\\\" app which makes awesome gifs and shares to your friends !!\",\n      \"minimumOsVersion\": \"5.1.1\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.xiaopeng.GifFree\",\n      \"trackName\": \"Animated Gif Camera Lite - Whats Funny yik Animated Gifs Creator App\",\n      \"trackId\": 960663264,\n      \"sellerName\": \"Wu Xiaopeng\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple20/v4/95/b4/12/95b412fe-1d9f-dd56-021a-28d8947817e1/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple18/v4/ab/dc/d1/abdcd151-533d-cfb5-cf00-4ab9025d9e5a/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple18/v4/91/7a/b3/917ab390-12ac-dffd-ecc5-d83830cfa261/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple18/v4/91/7a/b3/917ab390-12ac-dffd-ecc5-d83830cfa261/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple18/v4/91/7a/b3/917ab390-12ac-dffd-ecc5-d83830cfa261/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/hironori-tsumuraya/id1061365194?uo=4\",\n      \"advisories\": [\n        \"Unrestricted Web Access\"\n      ],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"Unread Checker for Slack\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"30683136\",\n      \"contentAdvisoryRating\": \"17+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/unread-checker-for-slack/id1134597935?mt=8&uo=4\",\n      \"trackContentRating\": \"17+\",\n      \"currentVersionReleaseDate\": \"2016-07-19T00:54:19Z\",\n      \"releaseNotes\": \"This update is signed with Apple’s latest signing certificate. No new features are included.\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2016-07-19T00:54:19Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.0.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1061365194,\n      \"artistName\": \"Hironori Tsumuraya\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"\\\"Unread Checker for Slack\\\" is fast and easy slack client! \\n\\nNo need switch teams. \\nNo need switch channels.\\nYou can check slack messages at one list as timeline!\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"tsumuchan.slack-unread-checker\",\n      \"trackName\": \"Unread Checker for Slack\",\n      \"trackId\": 1134597935,\n      \"sellerName\": \"Hironori Tsumuraya\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/e5/69/1d/e5691d09-7073-c34d-1b4d-e0d9132a9efa/source/320x180bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/29/f3/62/29f362ee-e0e0-cb21-7150-f046de80dfb8/source/320x180bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple4/v4/61/de/34/61de345c-3938-ac43-c6b2-694a56d3a05a/source/320x180bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple4/v4/41/a8/16/41a816dd-316e-e265-3241-4598b34b2f50/source/480x360bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/cc/da/ff/ccdaff7d-ccdc-6b8a-5d5d-6b8c303139ce/source/480x360bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple/v4/d6/17/a8/d617a851-4756-34a3-e6bd-df772bfdb6db/source/480x360bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/c0/0f/cf/c00fcfe8-c784-b8c2-0229-b2c02c80cc40/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/c0/0f/cf/c00fcfe8-c784-b8c2-0229-b2c02c80cc40/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/c0/0f/cf/c00fcfe8-c784-b8c2-0229-b2c02c80cc40/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/melanie-thomas/id775584887?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone3GS-iPhone-3GS\",\n        \"iPadWifi-iPadWifi\",\n        \"iPad3G-iPad3G\",\n        \"iPodTouchThirdGen-iPodTouchThirdGen\",\n        \"iPhone4-iPhone4\",\n        \"iPodTouchFourthGen-iPodTouchFourthGen\",\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"No Homework!\",\n      \"languageCodesISO2A\": [\n        \"CS\",\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"PL\",\n        \"PT\",\n        \"RU\",\n        \"ZH\",\n        \"ES\",\n        \"SV\",\n        \"ZH\",\n        \"TR\"\n      ],\n      \"fileSizeBytes\": \"14389248\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 2,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/no-homework/id896097193?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2014-07-13T04:51:43Z\",\n      \"primaryGenreId\": 6014,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6014\",\n        \"7009\",\n        \"7014\"\n      ],\n      \"releaseDate\": \"2014-07-13T04:51:43Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 775584887,\n      \"artistName\": \"melanie thomas\",\n      \"genres\": [\n        \"Games\",\n        \"Family\",\n        \"Role-Playing\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"It's summer holiday now! James is supposed to be doing his homework! But he doesn't want to--he wants to goof off! And besides, how could his mother be against her watering her flowers, and taking care of her other responsibilities? It's not all about homework, you know?\",\n      \"minimumOsVersion\": \"4.3\",\n      \"primaryGenreName\": \"Games\",\n      \"bundleId\": \"com.melaniethomas.NoHomework\",\n      \"trackName\": \"No Homework!\",\n      \"trackId\": 896097193,\n      \"sellerName\": \"melanie thomas\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/bd/6b/83/bd6b8301-cc32-59a6-2d4a-580095cd8b34/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/07/92/5a/07925af2-4d26-cf3f-dc89-f82e5665313b/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/9f/c9/92/9fc9928c-da8b-c7fb-9a9f-ca3a1f5f10db/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/16/64/ee/1664eed2-8b23-f424-3eb2-80f4304697fa/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/da/06/e1/da06e18a-82a5-a343-34c9-b5c67130bbb4/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/c2/7e/33/c27e33ac-8479-07c3-f3f5-5778460d3596/source/552x414bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/ca/9c/c7/ca9cc781-923d-37d2-aaf8-43cf0435c9ce/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/af/28/93/af289361-0d0a-5377-94cd-55fa738fb65d/source/552x414bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/08/83/16/08831622-b575-ca2d-6845-216529e8696b/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/49/5f/c4/495fc413-ec22-328a-770a-c11d56c3d923/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/49/5f/c4/495fc413-ec22-328a-770a-c11d56c3d923/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/49/5f/c4/495fc413-ec22-328a-770a-c11d56c3d923/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/broadsoft/id301242750?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"trackCensoredName\": \"Team-One\",\n      \"languageCodesISO2A\": [\n        \"NL\",\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"IT\",\n        \"JA\",\n        \"KO\",\n        \"ZH\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"122109952\",\n      \"sellerUrl\": \"http://www.team-one.com\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/team-one/id721334515?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-11-21T00:42:27Z\",\n      \"releaseNotes\": \"• Bug fixes and significant performance enhancements\",\n      \"primaryGenreId\": 6000,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6000\",\n        \"6007\"\n      ],\n      \"releaseDate\": \"2013-10-15T10:26:31Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"4.0.1\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 301242750,\n      \"artistName\": \"BroadSoft\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Team-One is more than messaging; it’s teamwork made simple.  \\n\\nOf course, you can chat but there’s so much more. Say goodbye to endless switching between apps and hello to a simpler way to work. Team-One’s persistent workspace model puts everything your teams need for better collaboration in one place. \\n\\u2028\\nWith Team-One, easily manage all your work in one place so you can be productive from anywhere, anytime, and on any device to stay in sync with your teams. Team-One supports your day from beginning to end.\\n\\nKEY FEATURES INCLUDE \\n\\t• Group and private chat \\n\\t• Persistent workspaces\\n\\t• Click-to-call*\\n\\t• Task management \\n\\t• Easy drag & drop file sharing \\n\\t• Note taking\\n\\t• Live meetings & screen sharing \\n\\t• Powerful search  \\n\\t• Email & calendar integrations \\n\\t• APIs & Bots\\n\\t• Integrations to many popular business apps including Dropbox, Google Drive, Salesforce, Jira, Marketo, Zendesk and more to assure that you have all the tools you need to collaborate \\n\\n*Requires separately purchased services \\n\\u2028\\nExperience how Team-One can make teamwork simple. \\n\\u2028\\n•• Highlights •• \\nAwarded by Frost & Sullivan in 2014 for Product Innovation and again in 2016 for Product Leadership in the Mobile Employee Collaboration Solutions space.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Business\",\n      \"bundleId\": \"net.intellinote.app.v2\",\n      \"trackName\": \"Team-One\",\n      \"trackId\": 721334515,\n      \"sellerName\": \"BroadSoft\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/06/0d/59/060d59c4-58fe-a0c8-8998-afc676fe8b2a/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/04/8d/61/048d61a3-d3a7-7579-affa-c206be86e7d2/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/b6/04/8c/b6048cdf-b6ea-993c-fb9d-21ef7fd33e5b/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/b0/4b/35/b04b355d-8878-ce6a-ec54-4f47a9e8e8eb/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/39/c5/12/39c512d1-6ced-fbe5-7f50-b62f067caba8/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/15/8d/0b/158d0b62-57d1-1a98-ea05-0c7b58b42b42/source/552x414bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/29/52/59/29525966-13de-671e-d14b-33c1e1ec97e4/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/29/52/59/29525966-13de-671e-d14b-33c1e1ec97e4/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/29/52/59/29525966-13de-671e-d14b-33c1e1ec97e4/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/constflash/id883373065?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"Widgets for Slack\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"8570880\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 1,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/widgets-for-slack/id1177337550?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-10-27T00:04:27Z\",\n      \"releaseNotes\": \"bug fixed\",\n      \"primaryGenreId\": 6007,\n      \"formattedPrice\": \"$5.99\",\n      \"genreIds\": [\n        \"6007\",\n        \"6000\"\n      ],\n      \"releaseDate\": \"2016-12-01T07:26:59Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.5\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 883373065,\n      \"artistName\": \"ConstFlash\",\n      \"genres\": [\n        \"Productivity\",\n        \"Business\"\n      ],\n      \"price\": 5.99,\n      \"description\": \"Now you can access all important information about your Slack's teams from your lock screen or any app. Add Widgets for Slack to the Notification Center - and you will get quick access to Slack's functions.\\n\\nWith this app you can see all unread messages, channels and people. \\nEasy navigation through sections will provide you quick access to helpful information.\\n\\nYou can jump directly from this widget to any channel of Slack's official app.\\n\\nYou can add up to 6 teams and control your business with ease.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Productivity\",\n      \"bundleId\": \"com.constflash.widgetslack\",\n      \"trackName\": \"Widgets for Slack\",\n      \"trackId\": 1177337550,\n      \"sellerName\": \"ConstFlash LTD\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/04/84/95/048495b3-535b-dde1-3317-2f4ee60fd248/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/dc/3f/6c/dc3f6c78-c925-2903-769e-3914a8ab53f7/source/392x696bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/e4/f3/cb/e4f3cb92-cd10-df5c-beb8-e087f6180ce0/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/4b/b3/00/4bb30029-2c5f-88cc-6688-fc380c22f649/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/4b/b3/00/4bb30029-2c5f-88cc-6688-fc380c22f649/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/4b/b3/00/4bb30029-2c5f-88cc-6688-fc380c22f649/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/sharath-prabhal/id1054565696?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5-iPhone5\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"Status Switcher for Slack\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"18738176\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/status-switcher-for-slack/id1234039479?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-04-08T20:18:41Z\",\n      \"releaseNotes\": \"Ability to disable custom emojis:\\nIf your organization contains a massive number(50k+) of custom emojis, the app may not be able to handle it. In that case, please disable custom emojis using the hamburger menu in the top left.\",\n      \"primaryGenreId\": 6002,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6002\",\n        \"6005\"\n      ],\n      \"releaseDate\": \"2017-05-12T04:00:11Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.0.4\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 1054565696,\n      \"artistName\": \"Sharath Prabhal\",\n      \"genres\": [\n        \"Utilities\",\n        \"Social Networking\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Do you use Slack's new status feature? Ever wondered how many taps it actually takes to update your status? It's 7, and it takes around 30 seconds! Now, you can update your status with just 1 tap, without even unlocking your phone! Perfect for anyone who uses Slack on the go. \\n\\n* Sign in with Slack\\n* Setup your top four status status message along with emojis\\n* Update, view and clear current status message from iOS Today Widget\\n* Set status without even unlocking your phone!\\n\\nIf your organization contains a massive number(50k+) of custom emojis, the app may not be able to handle it. In that case, please disable custom emojis using the hamburger menu in the top left.\",\n      \"minimumOsVersion\": \"10.0\",\n      \"primaryGenreName\": \"Utilities\",\n      \"bundleId\": \"com.sharath.slackStatusSwitcher\",\n      \"trackName\": \"Status Switcher for Slack\",\n      \"trackId\": 1234039479,\n      \"sellerName\": \"Sharath Prabhal\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/ef/77/09/ef770995-26b8-51be-b1d1-bbfc73fd0c18/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/26/78/e9/2678e94f-c8dd-f6bc-6de2-f508e5a9fa5d/source/392x696bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/11/aa/96/11aa96d4-9e2b-2bea-5d0b-b207e0c9bb1a/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/0d/e9/4c/0de94ca7-ca80-283d-b439-0c38f9b96037/source/392x696bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/57/ec/a0/57eca0a7-eb75-7a77-692e-0dbabe81a03d/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/95/d9/22/95d92242-4259-5a40-3a85-2cf642a40c5e/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/95/d9/22/95d92242-4259-5a40-3a85-2cf642a40c5e/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/95/d9/22/95d92242-4259-5a40-3a85-2cf642a40c5e/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/riding-horses-with-kathy-slack-llc/id573318806?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"Riding Horses\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"26295296\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/riding-horses/id573318803?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2017-08-14T15:02:21Z\",\n      \"releaseNotes\": \"• New Hunter / Jumper lesson collection\\n• Bug fixes and performance enhancements\",\n      \"primaryGenreId\": 6004,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6004\",\n        \"6017\"\n      ],\n      \"releaseDate\": \"2013-04-02T04:33:24Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.2\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 573318806,\n      \"artistName\": \"Riding Horses with Kathy Slack, LLC\",\n      \"genres\": [\n        \"Sports\",\n        \"Education\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Riding Horses with Kathy Slack puts your trainer in your pocket.\\n\\nRHWKS is a learn-while-you ride collection of Western horsemanship lessons. Each lesson goes with you to your arena and focuses on fundamental skill sets that every rider can master. Riders can purchase lessons based on their needs to truly customize their experience and training.  \\n\\nRide along to Kathy’s instruction using your iPhone or iPod and you’ll have access to this unique and successful teaching method. With a choice of audio/video lessons or audio with still photography, riders are given the tools they need to improve their skills and confidence in their own arena or backyard.  \\n\\nKathy Slack is the owner of two riding facilities in Austin, Texas where she teaches, trains and rides.  Since 1972, Kathy’s teaching enthusiasm and talent has produced a consistent stream of successful riders, many who have won national and world titles.  You can find Kathy serving as a board member of the USTPA, competing and coaching at events around the country, or on the web at www.ridinghorses.com\\n\\nStart Learning Today!\\n\\nApplication Features:\\n• Video lessons\\n• Audio only lessons plus still photography for easy reference\\n• Ability to play and pause lessons\\n• Audio controls available from lock screen for ease of use during riding\\n• Audio lessons are broken up into Objectives for convenient review\\n• In-App purchase options allow users to customize their collection\\n• High quality video and photography taken in Austin, Texas\\n\\nOver 50,000 people have already discovered Kathy’s unique and successful training methods.  Give Riding Horses with Kathy Slack a try today and enjoy the ease and flexibility of having your trainer in your pocket.\",\n      \"minimumOsVersion\": \"9.0\",\n      \"primaryGenreName\": \"Sports\",\n      \"bundleId\": \"com.ridinghorses.ridinghorses\",\n      \"trackName\": \"Riding Horses\",\n      \"trackId\": 573318803,\n      \"sellerName\": \"Riding Horses with Kathy Slack LLC\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/9d/ee/4a/9dee4ab5-3d3a-8a0c-96e1-c503925c39e5/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/e4/16/80/e4168008-db95-9072-de51-f5c0a541d7dd/source/392x696bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/dd/f7/b7/ddf7b761-8924-baa5-51ef-a9bc11e93530/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/8e/e8/13/8ee81383-603b-17ee-fe7d-2ac9ed8a8a6d/source/392x696bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/be/dd/da/beddda53-03a8-4e2b-0637-6af639445876/source/392x696bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/34/4c/d9/344cd995-97da-e82b-3ee4-8c666e09598b/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/34/4c/d9/344cd995-97da-e82b-3ee4-8c666e09598b/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/34/4c/d9/344cd995-97da-e82b-3ee4-8c666e09598b/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/blowbend-jp/id535422149?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [],\n      \"trackCensoredName\": \"Guitar Tuner TN-1G\",\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"DE\",\n        \"ID\",\n        \"IT\",\n        \"JA\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"76964864\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/guitar-tuner-tn-1g/id754125890?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2018-12-12T05:06:56Z\",\n      \"releaseNotes\": \"• Fixed an issue when launching the app.\\nI value your feedback, so if you have something to share then email me at tn1g.i@blowbend.jp. You can find a link to the mail in help.\",\n      \"primaryGenreId\": 6011,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6011\",\n        \"6002\"\n      ],\n      \"releaseDate\": \"2013-12-01T13:53:56Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.1.4\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 535422149,\n      \"artistName\": \"Blowbend.jp\",\n      \"genres\": [\n        \"Music\",\n        \"Utilities\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"It's very easy! Fine tuning for all guitarists.\\n\\nTN-1G is tuning meter only for guitarists.\\nIt detects the pitch of notes that picked up by a microphone, and it displays the difference from the right pitch. It's easy to use, but high-precision, high-performance, and supports alternative tuning sets of more than 50.\\n\\n◆It's really easy to use.\\n\\n◆High-precision, high-performance.\\n\\n◆Supported tuning-sets:\\n\\n  ・Standard, Half Step Down, Full Step Down, 1&1/2 Steps Down, Drop-D, Drop-D(half step down), Double Drop-D, Drop-G, Drop-C, Drop-C&G, Drop-C#, Drop-C(another set), Drop-B, Drop-A#, Drop-A, Open D, Open E, Open E7, Open Dm, Open Em, Open Em7, Open Dmaj7, Open Dadd9, Open G, Open A, Open A(another set), Open Gm, Open Am, Open Am7, Open G7, Open Gmaj7, G6, Dsus(DADGAD), Open C, C6, Open F, F6, Taro Patch, G Wahine, Leonard's C, C Major(Atta's C), Keola's C, Mauna Loa, Old Mauna Loa, Dmaj7 Wahine, F Wahine, Double Slack F, Modal G(Gsus), Open C(another set), Open C#, Open Cm, C Spanish and Perfect 4th\\n\\n◆It has a simple drum machine function.\\n  ・The rhythm pattern that you made, you can send to your friend by AirDrop or email.\\n  ・And also the data is a common use with TN-1B(bass tuner), you can share the rhythm pattern with the bassist of your band.\\n\\n◆You can change reference pitch-A between 438Hz and 445Hz.\\n\\n◆You can change note notation language.\\n  ・English(C, C#, D, E… , B)\\n  ・Italian(Do, Do#, Re, Mi… , Si)\\n  ・German(C, Cis, D, E… , H)\\n  ・French(Ut, Ut#, Ré, Mi… , Si)\\n  ・Chinese(C, 升C, D, E… , B)\\n\\n◆TN-1G has a tuning fork that emits a pitch corresponding to each string.\",\n      \"minimumOsVersion\": \"11.0\",\n      \"primaryGenreName\": \"Music\",\n      \"bundleId\": \"jp.blowbend.ios.music.TN1G\",\n      \"trackName\": \"Guitar Tuner TN-1G\",\n      \"trackId\": 754125890,\n      \"sellerName\": \"HIDEHISA YOKOYAMA\"\n    },\n    {\n      \"screenshotUrls\": [\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/cc/d2/a3/ccd2a339-7a05-0617-f8fb-edebb83d734a/source/320x180bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple6/v4/10/fa/8d/10fa8d93-a0bd-29c5-842d-084550c75b7c/source/320x180bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/d8/a6/78/d8a67817-c130-640e-b291-316adec7d276/source/320x180bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple4/v4/59/74/bc/5974bc0d-64e3-bbd5-a704-60237093278d/source/320x180bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple4/v4/4a/05/3b/4a053b48-e1bf-dc9f-c761-5b4b781abef7/source/320x180bb.jpg\"\n      ],\n      \"ipadScreenshotUrls\": [\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/bf/c9/21/bfc921a3-19bc-85a2-7bcd-8e2b7f880d02/source/480x360bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple6/v4/1a/a7/a9/1aa7a9e2-5209-8d46-b925-18b5d1176c91/source/480x360bb.jpg\",\n        \"https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/3b/07/e6/3b07e6e9-e439-593a-ec05-13e59584e1c6/source/480x360bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple/v4/15/1a/c3/151ac38d-6883-e28e-d3bc-55f9692fd77b/source/480x360bb.jpg\",\n        \"https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/a4/cc/18/a4cc185d-5685-ed2e-d4e5-f3cc4e7ae95d/source/480x360bb.jpg\"\n      ],\n      \"appletvScreenshotUrls\": [],\n      \"artworkUrl60\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/18/ea/12/18ea12b7-81c1-c937-f17f-a37884ab264a/source/60x60bb.jpg\",\n      \"artworkUrl512\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/18/ea/12/18ea12b7-81c1-c937-f17f-a37884ab264a/source/512x512bb.jpg\",\n      \"artworkUrl100\": \"https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/18/ea/12/18ea12b7-81c1-c937-f17f-a37884ab264a/source/100x100bb.jpg\",\n      \"artistViewUrl\": \"https://itunes.apple.com/au/developer/kory-foster/id730234819?uo=4\",\n      \"advisories\": [],\n      \"isGameCenterEnabled\": false,\n      \"kind\": \"software\",\n      \"supportedDevices\": [\n        \"iPhone3GS-iPhone-3GS\",\n        \"iPadWifi-iPadWifi\",\n        \"iPad3G-iPad3G\",\n        \"iPodTouchThirdGen-iPodTouchThirdGen\",\n        \"iPhone4-iPhone4\",\n        \"iPodTouchFourthGen-iPodTouchFourthGen\",\n        \"iPad2Wifi-iPad2Wifi\",\n        \"iPad23G-iPad23G\",\n        \"iPhone4S-iPhone4S\",\n        \"iPadThirdGen-iPadThirdGen\",\n        \"iPadThirdGen4G-iPadThirdGen4G\",\n        \"iPhone5-iPhone5\",\n        \"iPodTouchFifthGen-iPodTouchFifthGen\",\n        \"iPadFourthGen-iPadFourthGen\",\n        \"iPadFourthGen4G-iPadFourthGen4G\",\n        \"iPadMini-iPadMini\",\n        \"iPadMini4G-iPadMini4G\",\n        \"iPhone5c-iPhone5c\",\n        \"iPhone5s-iPhone5s\",\n        \"iPadAir-iPadAir\",\n        \"iPadAirCellular-iPadAirCellular\",\n        \"iPadMiniRetina-iPadMiniRetina\",\n        \"iPadMiniRetinaCellular-iPadMiniRetinaCellular\",\n        \"iPhone6-iPhone6\",\n        \"iPhone6Plus-iPhone6Plus\",\n        \"iPadAir2-iPadAir2\",\n        \"iPadAir2Cellular-iPadAir2Cellular\",\n        \"iPadMini3-iPadMini3\",\n        \"iPadMini3Cellular-iPadMini3Cellular\",\n        \"iPodTouchSixthGen-iPodTouchSixthGen\",\n        \"iPhone6s-iPhone6s\",\n        \"iPhone6sPlus-iPhone6sPlus\",\n        \"iPadMini4-iPadMini4\",\n        \"iPadMini4Cellular-iPadMini4Cellular\",\n        \"iPadPro-iPadPro\",\n        \"iPadProCellular-iPadProCellular\",\n        \"iPadPro97-iPadPro97\",\n        \"iPadPro97Cellular-iPadPro97Cellular\",\n        \"iPhoneSE-iPhoneSE\",\n        \"iPhone7-iPhone7\",\n        \"iPhone7Plus-iPhone7Plus\",\n        \"iPad611-iPad611\",\n        \"iPad612-iPad612\",\n        \"iPad71-iPad71\",\n        \"iPad72-iPad72\",\n        \"iPad73-iPad73\",\n        \"iPad74-iPad74\",\n        \"iPhone8-iPhone8\",\n        \"iPhone8Plus-iPhone8Plus\",\n        \"iPhoneX-iPhoneX\",\n        \"iPad75-iPad75\",\n        \"iPad76-iPad76\",\n        \"iPhoneXS-iPhoneXS\",\n        \"iPhoneXSMax-iPhoneXSMax\",\n        \"iPhoneXR-iPhoneXR\",\n        \"iPad812-iPad812\",\n        \"iPad834-iPad834\",\n        \"iPad856-iPad856\",\n        \"iPad878-iPad878\"\n      ],\n      \"features\": [\n        \"iosUniversal\"\n      ],\n      \"averageUserRatingForCurrentVersion\": 5.0,\n      \"trackCensoredName\": \"Thanksgiving Slacking\",\n      \"languageCodesISO2A\": [\n        \"EN\"\n      ],\n      \"fileSizeBytes\": \"2781184\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"userRatingCountForCurrentVersion\": 1,\n      \"trackViewUrl\": \"https://itunes.apple.com/au/app/thanksgiving-slacking/id738956061?mt=8&uo=4\",\n      \"trackContentRating\": \"4+\",\n      \"currentVersionReleaseDate\": \"2013-11-12T04:43:10Z\",\n      \"primaryGenreId\": 6014,\n      \"formattedPrice\": \"Free\",\n      \"genreIds\": [\n        \"6014\",\n        \"7009\",\n        \"7018\"\n      ],\n      \"releaseDate\": \"2013-11-12T04:43:10Z\",\n      \"currency\": \"AUD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"1.0\",\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"artistId\": 730234819,\n      \"artistName\": \"kory foster\",\n      \"genres\": [\n        \"Games\",\n        \"Family\",\n        \"Trivia\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Today we give thanks for everything wonderful in our lives. Sarah has to help prepare for her thanksgiving dinner, but can she behave or will she be caught by her mom?\\n\\nUse your finger to complete the activities. If your mom comes to check on you, click the close button to get back to work!\",\n      \"minimumOsVersion\": \"4.3\",\n      \"primaryGenreName\": \"Games\",\n      \"bundleId\": \"com.koryfoster.ThanksgivingSlacking\",\n      \"trackName\": \"Thanksgiving Slacking\",\n      \"trackId\": 738956061,\n      \"sellerName\": \"kory foster\"\n    }\n  ]\n}\n\n\n"
  },
  {
    "path": "testdata/itunes/mas-search-slack.json",
    "content": "{\n  \"resultCount\": 1,\n  \"results\": [\n    {\n      \"screenshotUrls\": [\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/8d/07/74/8d0774c5-90aa-611c-3701-35f6158fb77e/source/800x500bb.jpg\",\n        \"https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/6d/86/a7/6d86a74d-5c45-1a61-7828-ff3251360271/source/800x500bb.jpg\",\n        \"https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/18/c1/38/18c138de-5bf2-586b-34de-c81e05568ea3/source/800x500bb.jpg\",\n        \"https://is5-ssl.mzstatic.com/image/thumb/Purple118/v4/4a/92/50/4a925081-61d0-ff32-eb2a-4b57db03aaab/source/800x500bb.jpg\"\n      ],\n      \"artworkUrl512\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/84/94/74/84947420-25dc-35e2-2761-9fa2b583c0a5/source/512x512bb.png\",\n      \"artistViewUrl\": \"https://itunes.apple.com/us/developer/slack-technologies-inc/id453420243?mt=12&uo=4\",\n      \"artworkUrl60\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/84/94/74/84947420-25dc-35e2-2761-9fa2b583c0a5/source/60x60bb.png\",\n      \"artworkUrl100\": \"https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/84/94/74/84947420-25dc-35e2-2761-9fa2b583c0a5/source/100x100bb.png\",\n      \"kind\": \"mac-software\",\n      \"averageUserRatingForCurrentVersion\": 4.0,\n      \"languageCodesISO2A\": [\n        \"EN\",\n        \"FR\",\n        \"DE\",\n        \"JA\",\n        \"ES\"\n      ],\n      \"fileSizeBytes\": \"74398324\",\n      \"sellerUrl\": \"https://slack.com\",\n      \"userRatingCountForCurrentVersion\": 117,\n      \"trackContentRating\": \"4+\",\n      \"trackCensoredName\": \"Slack\",\n      \"trackViewUrl\": \"https://itunes.apple.com/us/app/slack/id803453959?mt=12&uo=4\",\n      \"contentAdvisoryRating\": \"4+\",\n      \"formattedPrice\": \"Free\",\n      \"releaseNotes\": \"All updates are important, of course. This one contains security updates, and as we know, they’re the most important kind of all.\",\n      \"currentVersionReleaseDate\": \"2018-10-02T23:28:05Z\",\n      \"releaseDate\": \"2014-01-23T02:46:20Z\",\n      \"sellerName\": \"Slack Technologies, Inc.\",\n      \"primaryGenreId\": 12001,\n      \"isVppDeviceBasedLicensingEnabled\": true,\n      \"currency\": \"USD\",\n      \"wrapperType\": \"software\",\n      \"version\": \"3.3.3\",\n      \"artistId\": 453420243,\n      \"artistName\": \"Slack Technologies, Inc.\",\n      \"genres\": [\n        \"Business\",\n        \"Productivity\"\n      ],\n      \"price\": 0.00,\n      \"description\": \"Slack brings team communication and collaboration into one place so you can get more work done, whether you belong to a large enterprise or a small business. Check off your to-do list and move your projects forward by bringing the right people, conversations, tools, and information you need together. Slack is available on any device, so you can find and access your team and your work, whether you’re at your desk or on the go.\\n\\nUse Slack to: \\n• Communicate with your team and organize your conversations by topics, projects, or anything else that matters to your work\\n• Message or call any person or group within your team\\n• Share and edit documents and collaborate with the right people all in Slack \\n• Integrate into your workflow, the tools and services you already use including Google Drive, Salesforce, Dropbox, Asana, Twitter, Zendesk, and more\\n• Easily search a central knowledge base that automatically indexes and archives your team’s past conversations and files\\n• Customize your notifications so you stay focused on what matters\\n\\nScientifically proven (or at least rumored) to make your working life simpler, more pleasant, and more productive. We hope you’ll give Slack a try.\\n\\nStop by and learn more at: https://slack.com/\",\n      \"primaryGenreName\": \"Business\",\n      \"genreIds\": [\n        \"12001\",\n        \"12014\"\n      ],\n      \"bundleId\": \"com.tinyspeck.slackmacgap\",\n      \"minimumOsVersion\": \"10.9\",\n      \"trackId\": 803453959,\n      \"trackName\": \"Slack\",\n      \"averageUserRating\": 4.0,\n      \"userRatingCount\": 1477\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "JSON_API_HEADERS = {\n    'Content-Type': 'application/vnd.api+json',\n    'Accept': 'application/vnd.api+json'\n}"
  },
  {
    "path": "tests/alembic_test.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# path to migration scripts\nscript_location = %(here)s/../commandment/alembic\n\nsqlalchemy.url = sqlite:///:memory:\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = INFO\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARNING\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = WARNING\nhandlers =\nqualname = alembic\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "tests/api/__init__.py",
    "content": ""
  },
  {
    "path": "tests/api/conftest.py",
    "content": "import pytest\nimport os\nfrom tests.conftest import *\nfrom commandment.models import Device\nfrom sqlalchemy.orm.session import Session\n\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\nTEST_DATA_DIR = os.path.realpath(TEST_DIR + '/../../testdata')\n\n\n@pytest.fixture(scope='function')\ndef device(session: Session):\n    \"\"\"Create a fixture device which is referenced in all of the fake MDM responses by its UDID.\"\"\"\n    d = Device(\n        udid='00000000-1111-2222-3333-444455556666',\n        device_name='commandment-mdmclient'\n    )\n    session.add(d)\n    session.commit()\n"
  },
  {
    "path": "tests/api/test_devices.py",
    "content": "import pytest\nimport os\nimport json\nimport sqlalchemy\nfrom flask import Response\nfrom tests.client import MDMClient\nfrom commandment.models import Command\n\n\n@pytest.mark.usefixtures(\"device\")\nclass TestDevicesAPI:\n\n    def test_patch_device_name(self, client: MDMClient, session):\n        \"\"\"Patching the device name should enqueue a Rename MDM command.\"\"\"\n        request_json = json.dumps({\n            \"data\": {\n                \"type\": \"devices\",\n                \"id\": \"1\",\n                \"attributes\": {\n                    \"device_name\": \"new name\"\n                }\n            },\n            \"jsonapi\": {\n                \"version\": \"1.0\"\n            }\n        })\n\n        response: Response = client.patch(\"/api/v1/devices/1\", data=request_json,\n                                          content_type=\"application/vnd.api+json\")\n        assert response.status_code == 200\n\n        try:\n            cmd: Command = session.query(Command).filter(Command.request_type == 'Settings').one()\n        except sqlalchemy.orm.exc.NoResultFound:\n            assert False, \"The API has created a new Settings Command to send to the device\"\n\n        device = json.loads(response.data)\n        assert device['data']['attributes']['device_name'] != \"new name\", \"Device rename is still pending, API should reflect old name\"\n\n    @pytest.mark.skip\n    def test_patch_hostname(self, client: MDMClient, session):\n        \"\"\"Patching the hostname should enqueue a Rename MDM command.\"\"\"\n        request_json = json.dumps({\n            \"data\": {\n                \"type\": \"devices\",\n                \"id\": \"1\",\n                \"attributes\": {\n                    \"hostname\": \"new name\"\n                }\n            },\n            \"jsonapi\": {\n                \"version\": \"1.0\"\n            }\n        })\n\n        response: Response = client.patch(\"/api/v1/devices/1\", data=request_json,\n                                          content_type=\"application/vnd.api+json\")\n        assert response.status_code == 200\n\n        try:\n            cmd: Command = session.query(Command).filter(Command.request_type == 'Settings').one()\n        except sqlalchemy.orm.exc.NoResultFound:\n            assert False, \"The API has created a new Settings Command to send to the device\"\n\n        device = json.loads(response.data)\n        assert device['data']['attributes']['hostname'] != \"new name\", \"Device rename is still pending, API should reflect old name\"\n\n    def test_patch_hostname_ios(self):\n        \"\"\"Patching an iOS device hostname should return 400 bad request.\"\"\"\n        pass\n\n    @pytest.mark.skip\n    def test_patch_device_name_reverted(self, client: MDMClient, session):\n        \"\"\"Patching the device name twice (change, then back to its original name)\n           should remove the queued Settings command.\"\"\"\n        request_json = json.dumps({\n            \"data\": {\n                \"type\": \"devices\",\n                \"id\": \"1\",\n                \"attributes\": {\n                    \"device_name\": \"new name\"\n                }\n            },\n            \"jsonapi\": {\n                \"version\": \"1.0\"\n            }\n        })\n\n        request_two_json = json.dumps({\n            \"data\": {\n                \"type\": \"devices\",\n                \"id\": \"1\",\n                \"attributes\": {\n                    \"device_name\": \"commandment-mdmclient\"\n                }\n            },\n            \"jsonapi\": {\n                \"version\": \"1.0\"\n            }\n        })\n\n        response: Response = client.patch(\"/api/v1/devices/1\", data=request_json,\n                                          content_type=\"application/vnd.api+json\")\n        assert response.status_code == 200\n\n        second_response: Response = client.patch(\"/api/v1/devices/1\", data=request_two_json,\n                                          content_type=\"application/vnd.api+json\")\n        assert second_response.status_code == 200\n\n        settings_commands = session.query(Command).filter(Command.request_type == 'Settings').count()\n        assert settings_commands == 1\n\n    def test_patch_device_name_coalesced(self, client: MDMClient, session):\n            \"\"\"Multiple device name changes should be coalesced into a single Settings command.\"\"\"\n            pass"
  },
  {
    "path": "tests/client.py",
    "content": "from flask.testing import FlaskClient\n\n\nclass MDMClient(FlaskClient):\n    \"\"\"MDMClient is a superset of the flask testing client meant to perform higher level operations similar to the\n    native mdmclient binary.\n    \n    Attributes:\n          _private_key (rsa.RSAPrivateKey): RSA Private Key for the simulated client.\n          _certificate (x509.Certificate): X.509 Certificate for the simulated client.\n    \"\"\"\n    \n    def __init__(self, *args, **kwargs):\n        self._private_key = kwargs.get('private_key', None)\n        self._certificate = kwargs.get('certificate', None)\n        super(MDMClient, self).__init__(*args, **kwargs)\n        #  self.environ_base['HTTP_MDM_SIGNATURE'] = b'Tk9UUkVBTA=='\n\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "import pytest\nimport os\nfrom flask import Flask\nfrom typing import Generator\nfrom commandment import create_app\nfrom commandment.models import db as _db\nfrom flask_sqlalchemy import SQLAlchemy\nfrom sqlalchemy.orm import scoped_session\nimport sqlalchemy\nfrom tests.client import MDMClient\nfrom alembic.command import upgrade\nfrom alembic.config import Config\n\n# For testing, every test uses an in-memory database with migrations that are applied in the fixture setup phase.\n# This ensures every test is fully isolated.\n\nTEST_DATABASE_URI = 'sqlite:///:memory:'\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\nALEMBIC_CONFIG = os.path.realpath(TEST_DIR + '/alembic_test.ini')\nTEST_APP_CONFIG = os.path.realpath(TEST_DIR + '/../travis-ci-settings.cfg')\n\n\n@pytest.yield_fixture(scope='function')\ndef app() -> Generator[Flask, None, None]:\n    \"\"\"Flask Application Fixture\"\"\"\n    a = create_app(TEST_APP_CONFIG)\n    a.config['TESTING'] = True\n    a.config['SQLALCHEMY_DATABASE_URI'] = TEST_DATABASE_URI\n\n    ctx = a.test_request_context()\n    ctx.push()\n\n    yield a\n\n    ctx.pop()\n\n\n@pytest.yield_fixture(scope='function')\ndef db(app: Flask) -> Generator[SQLAlchemy, None, None]:\n    \"\"\"Flask-SQLAlchemy Fixture\"\"\"\n    _db.app = app\n    #_db.create_all()\n    yield _db\n    # _db.drop_all()\n\n\n@pytest.yield_fixture(scope='function')\ndef session(db: SQLAlchemy) -> Generator[scoped_session, None, None]:\n    \"\"\"SQLAlchemy session Fixture\"\"\"\n    connection: sqlalchemy.engine.base.Connection = db.engine.connect()\n\n    with db.app.app_context():\n        config = Config(ALEMBIC_CONFIG)\n\n        # Issues with running upgrade() in a fixture with SQLite in-memory db:\n        # https://github.com/miguelgrinberg/Flask-Migrate/issues/153\n        #\n        # Basically: Alembic always spawns a new connection from upgrade() unless you specify a connection\n        # in config.attributes['connection']\n        config.attributes['connection'] = connection\n        upgrade(config, 'head')\n        connection.execute(\"SELECT * FROM devices\")\n\n    # transaction = connection.begin()\n\n    options = dict(bind=connection)\n    session = db.create_scoped_session(options=options)\n\n    db.session = session\n\n    yield session\n\n    # transaction.rollback()\n    session.remove()\n\n\n@pytest.fixture(scope='function')\ndef client(app: Flask) -> MDMClient:\n    \"\"\"Flask test client\"\"\"\n    app.test_client_class = MDMClient\n    test_client = app.test_client()\n    return test_client\n\n"
  },
  {
    "path": "tests/dep/__init__.py",
    "content": ""
  },
  {
    "path": "tests/dep/conftest.py",
    "content": "import pytest\nimport requests\nimport os.path\n\nfrom commandment.dep import SetupAssistantStep\nfrom commandment.dep.dep import DEP\nfrom commandment.dep.models import DEPProfile\nfrom commandment.models import Device\nfrom sqlalchemy.orm.session import Session\n\nSIMULATOR_URL = 'http://localhost:8080'\n\n\n@pytest.fixture\ndef simulator_token() -> dict:\n    res = requests.get('{}/token'.format(SIMULATOR_URL))\n    return res.json()\n\n\n@pytest.fixture\ndef live_token() -> str:\n    dep_token_path = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata', 'deptoken.json')\n    with open(dep_token_path, 'rb') as fd:\n        content = fd.read()\n\n    return content.decode('utf8')\n\n\n@pytest.fixture\ndef live_device() -> str:\n    return os.environ.get('DEP_DEVICE_UUID')\n\n\n@pytest.fixture\ndef live_dep_profile() -> str:\n    return os.environ.get('DEP_PROFILE_UUID')\n\n\n@pytest.fixture\ndef dep(simulator_token: dict) -> DEP:\n    d = DEP(\n        consumer_key=simulator_token['consumer_key'],\n        consumer_secret=simulator_token['consumer_secret'],\n        access_token=simulator_token['access_token'],\n        access_secret=simulator_token['access_secret'],\n        url=SIMULATOR_URL,\n    )\n\n    return d\n\n\n@pytest.fixture\ndef dep_live(live_token: str):\n    return DEP.from_token(live_token)\n\n\n@pytest.fixture\ndef dep_profile() -> dict:\n    p = {\n        'profile_name': 'Fixture Profile',\n        'url': 'https://localhost:5433',\n        'allow_pairing': True,\n        'is_supervised': True,\n        'is_multi_user': False,\n        'is_mandatory': False,\n        'await_device_configured': False,\n        'is_mdm_removable': True,\n        'support_phone_number': '12345678',\n        'auto_advance_setup': False,\n        'support_email_address': 'test@localhost',\n        'org_magic': 'COMMANDMENT-TEST-FIXTURE',\n        'skip_setup_items': [\n            SetupAssistantStep.AppleID,\n        ],\n        'department': 'Commandment Dept',\n        'devices': [],\n    }\n    return p\n\n\n@pytest.fixture\ndef dep_profile_committed(dep_profile: dict, session: Session):\n    dp = DEPProfile(**dep_profile)\n    session.add(dp)\n    session.commit()\n\n\n@pytest.fixture(scope='function')\ndef device(session: Session):\n    \"\"\"Create a fixture device which is referenced in all of the fake MDM responses by its UDID.\"\"\"\n    d = Device(\n        udid='00000000-1111-2222-3333-444455556666',\n        device_name='commandment-mdmclient'\n    )\n    session.add(d)\n    session.commit()\n"
  },
  {
    "path": "tests/dep/test_dep.py",
    "content": "import pytest\nfrom commandment.dep.dep import DEP\n\n\n@pytest.mark.depsim\nclass TestDEP:\n    def test_account(self, dep: DEP):\n        dep.fetch_token()\n        account = dep.account()\n        assert account is not None\n\n    def test_fetch_devices(self, dep: DEP):\n        dep.fetch_token()\n        devices = dep.fetch_devices()\n        assert len(devices) == 500\n        \n    # def test_device_details(self, dep: DEP):\n    #     dep.fetch_token()\n    #     device_details = dep.device_detail()\n\n    def test_fetch_cursor(self, dep: DEP):\n        dep.fetch_token()\n        for page in dep.devices():\n            print(len(page))\n            for d in page:\n                print(d)\n\n"
  },
  {
    "path": "tests/dep/test_dep_app.py",
    "content": "import pytest\nimport os\nimport json\nimport sqlalchemy\nfrom flask import Response\n\nfrom commandment.dep.models import DEPProfile\nfrom commandment.models import Device\nfrom tests.client import MDMClient\n\n\n@pytest.mark.dep\n@pytest.mark.usefixtures(\"device\", \"dep_profile_committed\")\nclass TestDEPAPI:\n\n    def test_post_dep_profile_relationship(self, client: MDMClient, session):\n        \"\"\"Test assignment of DEP Profile to device via relationship URL:\n            /api/v1/devices/<device_id>/relationships/dep_profiles\"\"\"\n        request_json = json.dumps({\n            \"data\": {\n                \"type\": \"dep_profiles\",\n                \"id\": \"1\",\n            },\n            \"jsonapi\": {\n                \"version\": \"1.0\"\n            }\n        })\n\n        response: Response = client.patch(\"/api/v1/devices/1/relationships/dep_profile\", data=request_json,\n                                          content_type=\"application/vnd.api+json\")\n        print(response.data)\n        assert response.status_code == 200\n\n        d: Device = session.query(Device).filter(Device.id == 1).one()\n        assert d.dep_profile_id is not None\n"
  },
  {
    "path": "tests/dep/test_dep_failures.py",
    "content": "import pytest\nfrom commandment.dep.dep import DEP\nfrom commandment.dep.errors import DEPServiceError\n\n@pytest.mark.depsim\nclass TestDEPFailures:\n    # NOTE: ensure that this is in exactly the same order as your DEPsim config.\n    @pytest.mark.parametrize(\"expected_status,expected_text\", [\n        (400, \"\"),\n        (403, \"ACCESS_DENIED\"),\n        (403, \"T_C_NOT_SIGNED\"),\n        (405, \"\"),\n        (401, \"UNAUTHORIZED\"),\n        (429, \"TOO_MANY_REQUESTS\"),\n    ])\n    def test_token_failure(self, dep: DEP, expected_status: int, expected_text: str):\n        try:\n            dep.fetch_token()\n        except DEPServiceError as e:\n            assert e.response.status_code == expected_status\n            assert e.text == expected_text\n\n    @pytest.mark.parametrize(\"expected_status,expected_text\", [\n        (403, \"ACCESS_DENIED\"),\n        (401, \"UNAUTHORIZED\"),\n    ])\n    def test_account_failure(self, dep: DEP, expected_status: int, expected_text: str):\n        try:\n            dep.fetch_token()\n            dep.account()\n        except DEPServiceError as e:\n            assert e.response.status_code == expected_status\n            assert e.text == expected_text\n\n\n"
  },
  {
    "path": "tests/dep/test_dep_live.py",
    "content": "import pytest\nfrom commandment.dep.dep import DEP\n\n\n@pytest.mark.dep\nclass TestDEPLive:\n    def test_account(self, dep_live: DEP):\n        dep_live.fetch_token()\n        account = dep_live.account()\n        assert account is not None\n        assert 'server_name' in account\n        assert 'server_uuid' in account\n        assert 'facilitator_id' in account\n        assert 'admin_id' in account\n        assert 'org_name' in account\n        assert 'org_email' in account\n        assert 'org_phone' in account\n        assert 'org_address' in account\n\n        # X-Server-Protocol 3\n        assert 'org_id' in account\n        assert 'org_id_hash' in account\n        assert 'org_type' in account\n        assert 'org_version' in account\n        \n    def test_fetch_devices(self, dep_live: DEP):\n        dep_live.fetch_token()\n        devices = dep_live.fetch_devices()\n        assert 'cursor' in devices\n        assert 'devices' in devices\n        assert 'fetched_until' in devices\n        assert 'more_to_follow' in devices\n        \n    def test_device_details(self, dep_live: DEP, live_device: str):\n        dep_live.fetch_token()\n        device_details = dep_live.device_detail(live_device)\n        print(device_details)\n\n    # def test_fetch_cursor(self, dep: DEP):\n    #     dep.fetch_token()\n    #     for page in dep.devices():\n    #         print(len(page))\n    #         for d in page:\n    #             print(d)\n\n    # def test_define_profile(self, dep_live: DEP, dep_profile: dict):\n    #     token = dep_live.fetch_token()\n    #     result = dep_live.define_profile(dep_profile)\n    #     assert 'profile_uuid' in result\n    #     assert 'devices' in result\n    #     # \n    #     print(result['profile_uuid'])\n\n    def test_get_profile(self, dep_live: DEP, live_dep_profile: str):\n        dep_live.fetch_token()\n        profiles = dep_live.profile(live_dep_profile)\n        print(profiles)\n"
  },
  {
    "path": "tests/dep/test_smime.py",
    "content": "import pytest\nimport os.path\nimport os\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives import serialization, hashes\nfrom commandment.dep import smime\n\nDEP_TOKEN_SMIME_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata', 'dep_smime.p7m')\nDEP_TOKEN_KEY_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata', 'dep_key.pem')\n\n\nclass TestDepSmime:\n    def test_decrypt(self):\n        with open(DEP_TOKEN_SMIME_PATH, 'rb') as fd:\n            message = fd.read()\n\n        with open(DEP_TOKEN_KEY_PATH, 'rb') as fd:\n            pem_key = fd.read()\n\n        pk = serialization.load_pem_private_key(\n            pem_key,\n            backend=default_backend(),\n            password=None,\n        )\n\n        result = smime.decrypt(message, pk)\n        print(result)\n"
  },
  {
    "path": "tests/mdm/__init__.py",
    "content": ""
  },
  {
    "path": "tests/mdm/conftest.py",
    "content": "import pytest\nimport os\nfrom tests.conftest import *\nfrom commandment.models import Device\nfrom sqlalchemy.orm.session import Session\n\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\nTEST_DATA_DIR = os.path.realpath(TEST_DIR + '/../../testdata')\n\n\n@pytest.fixture(scope='function')\ndef device(session: Session):\n    \"\"\"Create a fixture device which is referenced in all of the fake MDM responses by its UDID.\"\"\"\n    d = Device(\n        udid='00000000-1111-2222-3333-444455556666',\n        device_name='commandment-mdmclient'\n    )\n    session.add(d)\n    session.commit()\n\n\n@pytest.fixture()\ndef authenticate_request() -> str:\n    with open(os.path.join(TEST_DATA_DIR, 'Authenticate/10.12.2.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.fixture()\ndef tokenupdate_request() -> str:\n    with open(os.path.join(TEST_DATA_DIR, 'TokenUpdate/10.12.2.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.fixture()\ndef tokenupdate_user_request() -> str:\n    with open(os.path.join(TEST_DATA_DIR, 'TokenUpdate/10.12.2-user.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.fixture()\ndef checkout_request() -> str:\n    with open(os.path.join(TEST_DATA_DIR, 'CheckOut/10.11.x.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.fixture()\ndef available_os_updates_request() -> str:\n    with open(os.path.join(TEST_DATA_DIR, 'AvailableOSUpdates/10.12.5.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n"
  },
  {
    "path": "tests/mdm/test_available_os_updates.py",
    "content": "import pytest\nimport os\nimport plistlib\nfrom flask import Response\nfrom tests.client import MDMClient\nfrom commandment.mdm import CommandStatus\nfrom commandment.models import Command, Device\n\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\n\n\n@pytest.fixture(scope='function')\ndef available_os_updates_command(session):\n    \"\"\"Creates an AvailableOSUpdates command that has been sent to the fixture device so that it can be marked\n    acknowledged when the fake response comes in.\"\"\"\n    c = Command(\n        uuid='00000000-1111-2222-3333-444455556666',\n        request_type='AvailableOSUpdates',\n        status=CommandStatus.Sent.value,\n        parameters={},\n    )\n    session.add(c)\n    session.commit()\n\n\n@pytest.mark.usefixtures(\"device\", \"available_os_updates_command\")\nclass TestAvailableOSUpdates:\n\n    def test_available_os_updates_response(self, client: MDMClient, available_os_updates_request: str, session):\n        response: Response = client.put('/mdm', data=available_os_updates_request, content_type='text/xml')\n        assert response.status_code != 410\n        assert response.status_code == 200\n\n        d: Device = session.query(Device).filter(Device.udid == '00000000-1111-2222-3333-444455556666').one()\n        updates = d.available_os_updates\n\n        plist = plistlib.loads(available_os_updates_request.encode('utf8'))\n        assert len(updates) == len(plist['AvailableOSUpdates'])\n"
  },
  {
    "path": "tests/mdm/test_certificate_list.py",
    "content": "import pytest\nimport os\nfrom flask import Response\nfrom tests.client import MDMClient\nfrom commandment.mdm import CommandStatus\nfrom commandment.models import Command, Device\n\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\n\n\n@pytest.fixture()\ndef certificate_list_response():\n    with open(os.path.join(TEST_DIR, '../../testdata/CertificateList/10.11.x.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.fixture(scope='function')\ndef certificate_list_command(session):\n    c = Command(\n        uuid='00000000-1111-2222-3333-444455556666',\n        request_type='CertificateList',\n        status=CommandStatus.Sent.value,\n        parameters={},\n    )\n    session.add(c)\n    session.commit()\n\n\n@pytest.mark.usefixtures(\"device\", \"certificate_list_command\")\nclass TestCertificateList:\n\n    def test_certificate_list_response(self, client: MDMClient, certificate_list_response: str, session):\n        response: Response = client.put('/mdm', data=certificate_list_response, content_type='text/xml')\n        assert response.status_code != 410\n        assert response.status_code == 200\n\n        d = session.query(Device).filter(Device.udid == '00000000-1111-2222-3333-444455556666').one()\n        ic = d.installed_certificates\n        assert len(ic) == 2\n\n"
  },
  {
    "path": "tests/mdm/test_checkin.py",
    "content": "import pytest\nfrom flask import Response\nfrom tests.client import MDMClient\n\n\n@pytest.mark.usefixtures(\"device\")\nclass TestCheckin:\n\n    def test_authenticate(self, client: MDMClient, authenticate_request: str):\n        \"\"\"Basic test: Authenticate\"\"\"\n        response: Response = client.put('/checkin', data=authenticate_request, content_type='text/xml')\n        assert response.status_code != 410\n        assert response.status_code == 200\n        \n    def test_tokenupdate(self, client: MDMClient, tokenupdate_request: str):\n        \"\"\"Test a client attempting to update its token after being unenrolled is forced to unenroll via code 410.\"\"\"\n        response: Response = client.put('/checkin', data=tokenupdate_request, content_type='text/xml')\n        assert response.status_code != 200\n        assert response.status_code == 410\n\n    # def test_user_tokenupdate(self, client: MDMClient, tokenupdate_user_request: str):\n    #     \"\"\"Test a TokenUpdate message on the user channel.\"\"\"\n    #     response: Response = client.put('/checkin', data=tokenupdate_user_request, content_type='text/xml')\n    #     assert response.status_code != 410\n    #     assert response.status_code == 200\n\n    def test_checkout(self, client: MDMClient, checkout_request: str):\n        \"\"\"Test a CheckOut message\"\"\"\n        response: Response = client.put('/checkin', data=checkout_request, content_type='text/xml')\n        assert response.status_code != 410\n        assert response.status_code == 200\n"
  },
  {
    "path": "tests/mdm/test_device_information.py",
    "content": "import pytest\nimport os\nfrom flask import Response\nfrom tests.client import MDMClient\n\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\n\n\n@pytest.fixture()\ndef device_information_response():\n    with open(os.path.join(TEST_DIR, '../../testdata/DeviceInformation/10.11.x.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.mark.usefixtures(\"device\")\nclass TestDeviceInformation:\n\n    def test_device_information_response(self, client: MDMClient, device_information_response: str):\n        response: Response = client.put('/mdm', data=device_information_response, content_type='text/xml')\n        assert response.status_code != 410\n        assert response.status_code == 200\n"
  },
  {
    "path": "tests/mdm/test_installed_application_list.py",
    "content": "import pytest\nimport os\nfrom flask import Response\nfrom tests.client import MDMClient\nfrom commandment.mdm import CommandStatus\nfrom commandment.models import Command, Device\n\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\n\n\n@pytest.fixture()\ndef installed_application_list_response():\n    with open(os.path.join(TEST_DIR, '../../testdata/InstalledApplicationList/10.11.x.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.fixture(scope='function')\ndef installed_application_list_command(session):\n    c = Command(\n        uuid='00000000-1111-2222-3333-444455556666',\n        request_type='InstalledApplicationList',\n        status=CommandStatus.Sent.value,\n        parameters={},\n    )\n    session.add(c)\n    session.commit()\n\n\n@pytest.mark.usefixtures(\"device\", \"installed_application_list_command\")\nclass TestInstalledApplicationList:\n\n    def test_installed_application_list_response(self, client: MDMClient, installed_application_list_response: str, session):\n        response: Response = client.put('/mdm', data=installed_application_list_response, content_type='text/xml')\n        assert response.status_code != 410\n        assert response.status_code == 200\n\n        d: Device = session.query(Device).filter(Device.udid == '00000000-1111-2222-3333-444455556666').one()\n        ia = d.installed_applications\n        assert len(ia) == 3\n"
  },
  {
    "path": "tests/mdm/test_profile_list.py",
    "content": "import pytest\nimport os\nfrom flask import Response\nfrom tests.client import MDMClient\n\n\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\n\n\n@pytest.fixture()\ndef profile_list_response() -> str:\n    with open(os.path.join(TEST_DIR, '../../testdata/ProfileList/10.11.x.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.mark.usefixtures(\"device\")\nclass TestProfileList:\n\n    def test_profile_list_response(self, client: MDMClient, profile_list_response: str):\n        response: Response = client.put('/mdm', data=profile_list_response, content_type='text/xml')\n        assert response.status_code != 410\n        assert response.status_code == 200\n"
  },
  {
    "path": "tests/mdm/test_security_info.py",
    "content": "import pytest\nimport os\nfrom flask import Response\n\nfrom commandment.mdm import CommandStatus\nfrom tests.client import MDMClient\nfrom commandment.models import Command, Device\n\nTEST_DIR = os.path.realpath(os.path.dirname(__file__))\n\n\n@pytest.fixture()\ndef security_info_response():\n    with open(os.path.join(TEST_DIR, '../../testdata/SecurityInfo/10.11.x.xml'), 'r') as fd:\n        plist_data = fd.read()\n\n    return plist_data\n\n\n@pytest.fixture(scope='function')\ndef security_info_command(session):\n    c = Command(\n        uuid='00000000-1111-2222-3333-444455556666',\n        request_type='SecurityInfo',\n        status=CommandStatus.Sent.value,\n        parameters={},\n    )\n    session.add(c)\n    session.commit()\n\n\n@pytest.mark.usefixtures(\"device\", \"security_info_command\")\nclass TestSecurityInfo:\n\n    def test_security_info_response(self, client: MDMClient, security_info_response: str, session):\n        response: Response = client.put('/mdm', data=security_info_response, content_type='text/xml')\n        assert response.status_code != 410\n        assert response.status_code == 200\n\n        cmd = session.query(Command).filter(Command.uuid == '00000000-1111-2222-3333-444455556666').one()\n        assert CommandStatus(cmd.status) == CommandStatus.Acknowledged\n\n        d = session.query(Device).filter(Device.udid == '00000000-1111-2222-3333-444455556666').one()\n        assert not d.fde_enabled"
  },
  {
    "path": "tests/pkg/__init__.py",
    "content": ""
  },
  {
    "path": "tests/pki/__init__.py",
    "content": ""
  },
  {
    "path": "tests/pki/conftest.py",
    "content": "import pytest\nimport datetime\nfrom cryptography import x509\nfrom cryptography.x509.oid import NameOID\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.backends import default_backend\n\n\n@pytest.fixture\ndef private_key() -> rsa.RSAPrivateKey:\n    key = rsa.generate_private_key(\n        public_exponent=65537,\n        key_size=2048,\n        backend=default_backend(),\n    )\n    return key\n\n\n@pytest.fixture\ndef csr(private_key: rsa.RSAPrivateKey) -> x509.CertificateSigningRequest:\n    b = x509.CertificateSigningRequestBuilder()\n    req = b.subject_name(x509.Name([\n        x509.NameAttribute(NameOID.COUNTRY_NAME, u\"US\"),\n        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u\"CA\"),\n        x509.NameAttribute(NameOID.LOCALITY_NAME, u\"San Francisco\"),\n        x509.NameAttribute(NameOID.ORGANIZATION_NAME, u\"Commandment\"),\n        x509.NameAttribute(NameOID.COMMON_NAME, u\"Commandment\"),\n    ])).sign(private_key, hashes.SHA256(), default_backend())\n\n    return req\n\n\n@pytest.fixture\ndef certificate(private_key: rsa.RSAPrivateKey) -> x509.Certificate:\n    b = x509.CertificateBuilder()\n    name = x509.Name([\n        x509.NameAttribute(NameOID.COUNTRY_NAME, u\"US\"),\n        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u\"CA\"),\n        x509.NameAttribute(NameOID.LOCALITY_NAME, u\"San Francisco\"),\n        x509.NameAttribute(NameOID.ORGANIZATION_NAME, u\"Commandment\"),\n        x509.NameAttribute(NameOID.COMMON_NAME, u\"CA-CERTIFICATE\"),\n    ])\n\n    cer = b.subject_name(name).issuer_name(name).public_key(\n        private_key.public_key()\n    ).serial_number(1).not_valid_before(\n        datetime.datetime.utcnow()\n    ).not_valid_after(\n        datetime.datetime.utcnow() + datetime.timedelta(days=10)\n    ).add_extension(\n        x509.BasicConstraints(ca=False, path_length=None), True\n    ).sign(private_key, hashes.SHA256(), default_backend())\n\n    return cer\n\n\n@pytest.fixture\ndef ca_certificate(private_key: rsa.RSAPrivateKey) -> x509.Certificate:\n    b = x509.CertificateBuilder()\n    name = x509.Name([\n        x509.NameAttribute(NameOID.COUNTRY_NAME, u\"US\"),\n        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u\"CA\"),\n        x509.NameAttribute(NameOID.LOCALITY_NAME, u\"San Francisco\"),\n        x509.NameAttribute(NameOID.ORGANIZATION_NAME, u\"Commandment\"),\n        x509.NameAttribute(NameOID.COMMON_NAME, u\"CA-CERTIFICATE\"),\n    ])\n\n    cert = b.serial_number(1).issuer_name(\n        name\n    ).subject_name(\n        name\n    ).public_key(\n        private_key.public_key()\n    ).not_valid_before(\n        datetime.datetime.utcnow()\n    ).not_valid_after(\n        datetime.datetime.utcnow() + datetime.timedelta(days=10)\n    ).add_extension(\n        x509.BasicConstraints(ca=True, path_length=None), True\n    ).sign(private_key, hashes.SHA256(), default_backend())\n\n    return cert\n\n"
  },
  {
    "path": "tests/pki/test_ca.py",
    "content": ""
  },
  {
    "path": "tests/pki/test_models.py",
    "content": "import pytest\nimport os.path\nimport logging\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom commandment.pki.models import RSAPrivateKey, CACertificate\n\nlogger = logging.getLogger(__name__)\n\n\nclass TestModels:\n\n    def test_rsa_privatekey_from_crypto(self, private_key: rsa.RSAPrivateKeyWithSerialization, session):\n        m = RSAPrivateKey.from_crypto(private_key)\n        session.add(m)\n        session.commit()\n\n        assert m.id is not None\n        assert m.pem_data is not None\n\n    def test_ca_certificate_from_crypto(self, ca_certificate: x509.Certificate, session):\n        m = CACertificate.from_crypto(ca_certificate)\n        session.add(m)\n        session.commit()\n\n        assert m.id is not None\n        assert m.pem_data is not None\n        assert m.fingerprint is not None\n        assert m.x509_cn is not None\n\n"
  },
  {
    "path": "tests/pki/test_openssl.py",
    "content": "import pytest\nimport os.path\nimport logging\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom commandment.pki import openssl\nimport oscrypto\n\nlogger = logging.getLogger(__name__)\n\n\nclass TestOpenssl:\n\n    def test_pkcs12_from_crypto(self, private_key: rsa.RSAPrivateKeyWithSerialization, certificate: x509.Certificate):\n        pkcs12_data = openssl.create_pkcs12(private_key, certificate)\n\n\n"
  },
  {
    "path": "tests/pki/test_ormutils.py",
    "content": "import pytest\nimport os.path\nimport logging\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom commandment.pki.models import RSAPrivateKey, CACertificate\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass TestORMUtils:\n\n    def test_find_recipient(self, certificate):\n        pass\n"
  },
  {
    "path": "tests/test_api_flat.py",
    "content": "import pytest\nfrom flask import Flask\nfrom commandment import create_app\nfrom commandment.models import db\nimport tempfile\nimport json\nfrom . import JSON_API_HEADERS\n\n\nclass TestApiCertificates:\n    pass\n    # def test_get_certificates(self, app):\n    #     res = app.get('/api/v1/certificates/?size=50&number=1', headers=JSON_API_HEADERS)\n    #     rd = json.loads(res.data)\n    #     print(rd)\n    #\n    #     assert res.status_code == 200\n    # def test_post_push_certificate(self, app):\n    #     res = app.get('/api/v1/push_certificate', headers={\n    #         'Content-Type': 'application/vnd.api+json',\n    #         'Accept': 'application/vnd.api+json'\n    #     })\n    #\n    # assert res.status_code == 201\n\n\n    # def test_get_push_certificate(self, app):\n    #     res = app.get('/api/v1/push_certificate', headers={\n    #                 'Content-Type': 'application/vnd.api+json',\n    #                 'Accept': 'application/vnd.api+json'\n    #     })\n    #     print(res.data)\n    #     assert res.status_code == 200\n\n    # def test_post_push_certificate_pkcs12(self, app, pkcs12_certificate):\n    #     \"\"\"Assert that a PKCS#12 can be posted to the push certificate endpoint.\"\"\"\n    #     res = app.post('/api/v1/push_certificate', headers={\n    #         'Content-Type': 'application/x-pkcs12',\n    #         'Accept': 'application/json'\n    #     }, data=pkcs12_certificate)\n    #     print(res.data)\n\n\n\n    # def test_post_certificate_signing_request(self, app):\n    #     res = app.post('/api/v1/certificate_signing_requests', headers={\n    #         'Content-Type': 'application/vnd.api+json',\n    #         'Accept': 'application/vnd.api+json'\n    #     }, data=json.dumps({\n    #         'data': {\n    #             'type': 'certificate_signing_requests',\n    #             'attributes': {\n    #                 'purpose': 'mdm.pushcert',\n    #                 'subject': 'O=commandment/OU=IT/CN=commandment.dev'\n    #             }\n    #         }\n    #     }))\n    #     print(res.data)\n    #     assert res.status_code == 201\n"
  },
  {
    "path": "tests/test_mdmcert.py",
    "content": "import pytest\nimport os.path\nimport logging\nfrom cryptography import x509\nfrom cryptography.x509.oid import NameOID\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.backends import default_backend\nfrom commandment.apns.mdmcert import submit_mdmcert_request\n\nENCRYPTION_CERT = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'mdmcert-encryption.cer')\n\nlogger = logging.getLogger(__name__)\n\n@pytest.fixture\ndef private_key() -> rsa.RSAPrivateKey:\n    key = rsa.generate_private_key(\n        public_exponent=65537,\n        key_size=2048,\n        backend=default_backend(),\n    )\n    return key\n\n\n@pytest.fixture\ndef csr(private_key: rsa.RSAPrivateKey) -> x509.CertificateSigningRequest:\n    b = x509.CertificateSigningRequestBuilder()\n    req = b.subject_name(x509.Name([\n        x509.NameAttribute(NameOID.COUNTRY_NAME, u\"US\"),\n        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u\"CA\"),\n        x509.NameAttribute(NameOID.LOCALITY_NAME, u\"San Francisco\"),\n        x509.NameAttribute(NameOID.ORGANIZATION_NAME, u\"Commandment\"),\n        x509.NameAttribute(NameOID.COMMON_NAME, u\"Commandment\"),\n    ])).sign(private_key, hashes.SHA256(), default_backend())\n\n    return req\n\n\n@pytest.fixture\ndef encryption_cert() -> x509.Certificate:\n    with open(ENCRYPTION_CERT, 'rb') as fd:\n        certdata = fd.read()\n\n    cert = x509.load_pem_x509_certificate(certdata, default_backend())\n    return cert\n\n\n# class TestMDMCert:\n#     def test_submit_mdmcert_request(self, csr: x509.CertificateSigningRequest, encryption_cert: x509.Certificate):\n#         res = submit_mdmcert_request(\"admin@localhost\", csr, encryption_cert)\n#         assert res['result'] == 'success'\n"
  },
  {
    "path": "tests/threads/__init__.py",
    "content": ""
  },
  {
    "path": "tests/threads/test_startup_thread.py",
    "content": "import pytest\nfrom commandment.threads import startup_thread\nfrom commandment.pki.models import CACertificate\n\n\nclass TestStartupThread:\n\n    def test_startup_thread_ca(self, session):\n        \"\"\"Assert that the startup thread actually creates self-signed certificates.\"\"\"\n        startup_thread.startup_callback()\n        certificate = session.query(CACertificate).one()\n        assert certificate.x509_cn == 'COMMANDMENT-CA'\n        assert certificate.pem_data is not None\n        assert certificate.fingerprint is not None\n"
  },
  {
    "path": "tests/vpp/__init__.py",
    "content": ""
  },
  {
    "path": "tests/vpp/conftest.py",
    "content": "import pytest\nimport requests\nfrom commandment.vpp.vpp import VPP\n\n\nSIMULATOR_URL = 'http://localhost:8080'\n\nSERVICE_CONFIG = {\n    \"associateLicenseSrvUrl\": \"http://localhost:8080/associateVPPLicenseSrv\",\n    \"clientConfigSrvUrl\": \"http://localhost:8080/VPPClientConfigSrv\",\n    \"contentMetadataLookupUrl\": \"https://uclient-api.itunes.apple.com/WebObjects/MZStorePlatform.woa/wa/lookup\",\n    \"disassociateLicenseSrvUrl\": \"http://localhost:8080/disassociateVPPLicenseSrv\",\n    \"editUserSrvUrl\": \"http://localhost:8080/editVPPUserSrv\",\n    \"getLicensesSrvUrl\": \"http://localhost:8080/getVPPLicensesSrv\",\n    \"getUserSrvUrl\": \"http://localhost:8080/getVPPUserSrv\",\n    \"getUsersSrvUrl\": \"http://localhost:8080/getVPPUsersSrv\",\n    \"getVPPAssetsSrvUrl\": \"http://localhost:8080/getVPPAssetsSrv\",\n    \"invitationEmailUrl\": \"http://buy.itunes.apple.com/us/vpp-associate?inviteCode=%25inviteCode%25\\u0026mt=8\",\n    \"manageVPPLicensesByAdamIdSrvUrl\": \"http://localhost:8080/manageVPPLicensesByAdamIdSrv\",\n    \"maxBatchAssociateLicenseCount\": 10,\n    \"maxBatchDisassociateLicenseCount\": 10,\n    \"registerUserSrvUrl\": \"http://localhost:8080/registerVPPUserSrv\",\n    \"retireUserSrvUrl\": \"http://localhost:8080/retireVPPUserSrv\",\n    \"status\": 0,\n    \"vppWebsiteUrl\": \"https://vpp.itunes.apple.com/\"\n}\n\n\n@pytest.fixture\ndef simulator_token() -> str:\n    res = requests.get('{}/internal/get_stoken'.format(SIMULATOR_URL))\n    return res.json().get('sToken', None)\n\n\n@pytest.fixture()\ndef vpp(simulator_token: str) -> VPP:\n    return VPP(\n        stoken=simulator_token,\n        vpp_service_config_url='http://localhost:8080/VPPServiceConfigSrv',\n        service_config=SERVICE_CONFIG\n    )\n"
  },
  {
    "path": "tests/vpp/vpp_test.py",
    "content": "import pytest\nimport logging\n\nfrom commandment.vpp.enum import LicenseAssociationType\nfrom commandment.vpp.vpp import VPP\n\nlogger = logging.getLogger(__name__)\n\nVPP_MOCK_USER_CID = 'F33D9E0F-CDE3-427E-A444-B137BEF9EFA2'\nVPP_MOCK_USER_ID = 2878111686099947\nVPP_MOCK_USER_EMAIL = 'vpp-test@localhost'\nVPP_MOCK_USER_EMAIL_2 = 'vpp-test-2@localhost'\nVPP_BATCH_LICENSE_ADAMID = 525463029  # This license is used as the test for large batch operations\n\n\n@pytest.mark.vppsim\nclass TestVPP:\n\n    # def test_vpp_init(self, vpp):\n    #     assert vpp is not None\n\n    def test_vpp_register_user(self, vpp: VPP):\n        reply = vpp.register_user(VPP_MOCK_USER_CID, VPP_MOCK_USER_EMAIL)\n        assert reply['status'] == 0\n        assert 'user' in reply\n\n    def test_getuser_by_client_id(self, vpp: VPP):\n        reply = vpp.get_user(client_user_id=VPP_MOCK_USER_CID)\n        assert reply['status'] == 0\n        assert 'user' in reply\n\n    # def test_getuser_by_client_id_with_itshash(self, vpp):\n    #     reply = vpp.get_user(client_user_id=VPP_MOCK_USER_ID, its_id_hash='')\n\n    def test_getuser_by_user_id(self, vpp: VPP):\n        reply = vpp.get_user(user_id=VPP_MOCK_USER_ID)\n        assert reply['status'] == 0\n        assert 'user' in reply\n\n    def test_retireuser_by_client_id(self, vpp: VPP):\n        reply = vpp.retire_user(client_user_id=VPP_MOCK_USER_CID)\n        assert reply['status'] == 0\n\n    # def test_already_retired_by_client_id(self, vpp: VPP):\n    #     reply = vpp.retire_user(client_user_id=VPP_MOCK_USER_CID)\n    #     assert reply['status'] == 0\n\n    # def test_retireuser_by_user_id(self, vpp: VPP):\n    #     reply = vpp.retire_user(client_user_id=VPP_MOCK_USER_CID)\n    #     assert reply['status'] == 0\n\n    def test_edit_user_by_client_id(self, vpp: VPP):\n        reply = vpp.edit_user(client_user_id=VPP_MOCK_USER_CID, email=VPP_MOCK_USER_EMAIL_2)\n        assert reply['status'] == 0\n        assert reply['user']['email'] == VPP_MOCK_USER_EMAIL_2\n\n    def test_get_assets(self, vpp: VPP):\n        reply = vpp.assets()\n        assert 'assets' in reply\n\n    # def test_get_licenses(self, vpp: VPP):\n    #     licenses = vpp.licenses()\n    #     print(licenses)\n\n    def test_users(self, vpp: VPP):\n        cursor = vpp.users()\n        while cursor.next():\n            users = cursor.users\n            print(users)\n\n        print('cursor exhausted')\n\n    def test_licenses(self, vpp: VPP):\n        cursor = vpp.licenses(VPP_BATCH_LICENSE_ADAMID)\n        licenses = []\n        total = cursor.total\n        assert len(cursor.licenses) == 600\n        licenses = licenses + cursor.licenses\n\n        while cursor.next():\n            licenses = licenses + cursor.licenses\n\n        assert len(licenses) == total\n\n    def test_manage_one_license(self, vpp: VPP):\n        op = vpp.manage(VPP_BATCH_LICENSE_ADAMID)\n\n        op.add(LicenseAssociationType.ClientUserID, VPP_MOCK_USER_CID)\n        vpp.save(op)\n        "
  },
  {
    "path": "travis-ci-settings.cfg",
    "content": "# This settings file will be used only in Travis CI\n\nfrom os import path\ndirname = path.dirname(__file__)\n\n# The public facing hostname of the MDM\n# This will also be used as the self signed certificate dnsname\nPUBLIC_HOSTNAME = 'commandment.dev'\n\n# Development mode listen port\nPORT = 5443\n\n# Configure your Database URI.\n# All SQLAlchemy options are available here:\n# http://flask-sqlalchemy.pocoo.org/2.1/config/\nSQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'\n\n\n\n# You may supply the certificate as a pair of PEM encoded files, or as a .p12 container.\n# If you supply .p12 it will be encoded as a PEM keypair\nPUSH_CERTIFICATE = '../push.pem'\nPUSH_KEY = '../push.key'\nPUSH_CERTIFICATE_PASSWORD = 'sekret'  # for pkcs12 only\n\n# If commandment is running in development mode, specify the path to the certificate and private key.\n# These can also be generated at start up.\n# Normally SSL should be handled by Apache/Nginx/etc.\n\n# Specify the Enterprise CA here if Apple Devices won't natively trust your CA.\nCA_CERTIFICATE = path.join(dirname, 'ssl', 'ca.crt')\nSSL_CERTIFICATE = path.join(dirname, 'ssl', 'server.crt')\nSSL_RSA_KEY = path.join(dirname, 'ssl', 'server.key')\n\n# If not using external storage, the path to the root directory for upload storage.\n# This should not be used in production.\n\nSTORAGE_ROOT = path.join(dirname, 'storage')\n"
  },
  {
    "path": "ui/.eslintrc.js",
    "content": "module.exports = {\n  \"env\": {\n    \"browser\": true,\n    \"es6\": true\n  },\n  \"extends\": \"eslint:recommended\",\n  \"globals\": {\n    \"Atomics\": \"readonly\",\n    \"SharedArrayBuffer\": \"readonly\"\n  },\n  \"parser\": \"@typescript-eslint/parser\",\n  parserOptions: {\n    \"ecmaFeatures\": {\n      \"jsx\": true\n    },\n    ecmaVersion: 2018,\n    sourceType: 'module',\n  },\n  \"plugins\": [\n    \"react\",\n    \"@typescript-eslint\"\n  ],\n  \"rules\": {}\n};"
  },
  {
    "path": "ui/.gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n### Node template\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n\n"
  },
  {
    "path": "ui/.storybook/config.js",
    "content": "import { configure } from '@storybook/react';\n\nconst req = require.context('../src/stories', true, /.stories.tsx$/);\n\nfunction loadStories() {\n  require('../src/stories/index.ts');\n}\n\nconfigure(loadStories, module);\n"
  },
  {
    "path": "ui/.storybook/preview-head.html",
    "content": "<!-- Semantic UI CDN -->\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/semantic-ui@2.2.13/dist/semantic.min.css\">\n\n"
  },
  {
    "path": "ui/.storybook/webpack.config.js",
    "content": "const path = require('path');\n\nconst {CheckerPlugin} = require('awesome-typescript-loader');\n\nmodule.exports = (baseConfig, env, defaultConfig) => {\n  defaultConfig.module.rules.push({\n    test: /\\.tsx?$/,\n    include: path.resolve(__dirname, '../src'),\n    loader: require.resolve('awesome-typescript-loader')\n  });\n  defaultConfig.plugins.push(new CheckerPlugin());\n  defaultConfig.resolve.extensions.push('.ts', '.tsx');\n\n  // defaultConfig.module.rules.push({\n  //   test: /\\.jsx?$/,\n  //   include: [\n  //     path.resolve(__dirname, \"node_modules/semantic-ui-react\"),\n  //     path.resolve(__dirname, \"node_modules/byte-size\")\n  //   ],\n  //   loader: \"babel-loader\"\n  // });\n\n  return defaultConfig;\n};\n\n"
  },
  {
    "path": "ui/_deprecated/AssistantPage.tsx",
    "content": "import * as React from 'react';\nimport { connect, Dispatch } from 'react-redux';\nimport { bindActionCreators } from 'redux';\nimport {RouteComponentProps} from 'react-router';\nimport {Assistant} from '../src/components/_deprecated/Assistant';\nimport {nextStep, prevStep} from '../src/actions/assistant';\nimport {newCertificateSigningRequest} from '../src/actions/signing_requests';\nimport {NextStepAction, PrevStepAction} from \"../src/actions/assistant\";\nimport {APNSConfiguration} from './assistant/APNSConfiguration';\nimport {SSLConfiguration} from \"./assistant/SSLConfiguration\";\nimport {SCEPConfiguration} from \"./assistant/SCEPConfiguration\";\nimport {FinalStep} from \"./assistant/FinalStep\";\nimport {RootState} from \"../src/reducers/index\";\nimport {AssistantState} from \"../src/reducers/assistant\";\n\ninterface AssistantPageStateProps {\n    assistant: AssistantState;\n}\n\ninterface AssistantPageDispatchProps {\n    nextStep: () => NextStepAction;\n    prevStep: () => PrevStepAction;\n    newCertificateSigningRequest: (purpose: string) => void;\n}\n\ninterface OwnProps {\n    handleGenerateSSLCSR: () => void;\n}\n\ninterface AssistantPageProps extends AssistantPageDispatchProps, AssistantPageStateProps, RouteComponentProps<any> {\n\n}\n\n@connect<AssistantPageStateProps, AssistantPageDispatchProps, OwnProps>(\n    (state: RootState, ownProps?: any): AssistantPageStateProps => {\n        return {assistant: state.assistant};\n    },\n    (dispatch: Dispatch<any>): AssistantPageDispatchProps => {\n        return bindActionCreators({\n            nextStep,\n            prevStep,\n            newCertificateSigningRequest\n        }, dispatch);\n    }\n)\nexport class AssistantPage extends React.Component<AssistantPageProps, undefined> {\n    \n    handleGenerateSSLCSR = (): void => {\n        console.log('generating an SSL CSR');\n        this.props.newCertificateSigningRequest('ssl');\n    };\n\n    render() {\n        const {\n            children,\n            currentStep,\n            totalSteps,\n            nextStep,\n            prevStep\n        } = this.props;\n\n        const steps = [\n            <APNSConfiguration />,\n            <SSLConfiguration onClickGenerateCSR={this.handleGenerateSSLCSR} />,\n            <SCEPConfiguration />,\n            <FinalStep />\n        ];\n\n        return (\n            <div className='AssistantPage container top-margin'>\n                <Assistant\n                    currentStep={currentStep}\n                    totalSteps={steps.length}\n                    onClickNext={nextStep}\n                    onClickPrev={prevStep}\n                    components={steps}\n                />\n            </div>\n        )\n    }\n\n}"
  },
  {
    "path": "ui/_deprecated/DeviceGroupPage.tsx",
    "content": "import * as React from 'react';\nimport {connect, Dispatch} from 'react-redux';\nimport {RootState} from \"../src/reducers/index\";\nimport {bindActionCreators} from \"redux\";\nimport Container from \"semantic-ui-react/src/elements/Container\";\nimport Header from \"semantic-ui-react/src/elements/Header\";\nimport {DeviceGroupForm, FormData as DeviceGroupFormData} from \"../src/forms/DeviceGroupForm\";\nimport {\n    post, PostActionRequest,\n    read, ReadActionRequest\n} from \"../src/actions/device_groups\";\nimport {RouteComponentProps} from \"react-router\";\nimport Griddle, {RowDefinition, ColumnDefinition} from 'griddle-react';\nimport {DeviceGroup} from \"../src/store/device_groups/types\";\nimport {JSONAPIDetailResponse} from \"../src/json-api\";\nimport {SemanticUIPlugin} from \"../src/griddle-plugins/semantic-ui/index\";\nimport {SimpleLayout as Layout} from \"../src/components/griddle/SimpleLayout\";\nimport {Device} from \"../src/store/device/types\";\n\ninterface RouteProps {\n    id?: string;\n}\n\ninterface OwnProps extends RouteComponentProps<RouteProps> {\n    handleSubmit: (values: DeviceGroupFormData) => void;\n}\n\ninterface ReduxStateProps {\n    device_group: JSONAPIDetailResponse<DeviceGroup, Device>;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: OwnProps): ReduxStateProps {\n    return {\n        device_group: state.device_groups.editing\n    };\n}\n\ninterface ReduxDispatchProps {\n    post: PostActionRequest;\n    read: ReadActionRequest;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch<RootState>, ownProps?: OwnProps) {\n    return bindActionCreators({\n        post,\n        read\n    }, dispatch);\n}\n\n\nclass BaseDeviceGroupPage extends React.Component<ReduxStateProps & ReduxDispatchProps & OwnProps, void> {\n\n    componentWillMount?() {\n        if (this.props.match.params.id) {\n            this.props.read(this.props.match.params.id, ['devices']);\n        }\n    }\n\n    handleSubmit = (values: DeviceGroupFormData) => {\n        if (this.props.match.params.id) {\n            // this.props.patch()\n        } else {\n            this.props.post(values);\n        }\n    };\n\n    render() {\n        const {\n            device_group\n        } = this.props;\n\n        let initialValues: any;\n        if (device_group) {\n            initialValues = device_group.data.attributes;\n        }\n\n        return (\n            <Container>\n                <Header as='h1'>Device Group</Header>\n                <DeviceGroupForm onSubmit={this.handleSubmit} initialValues={initialValues}/>\n                <Header as='h2'>Members</Header>\n                <Griddle\n                    plugins={[SemanticUIPlugin()]}\n                    styleConfig={{\n                        classNames: {\n                            Table: 'ui celled table',\n                            NoResults: 'ui message'\n                        }\n                    }}\n                    components={{Layout}}\n                >\n\n                </Griddle>\n            </Container>\n        )\n    }\n}\n\nexport const DeviceGroupPage = connect<ReduxStateProps, ReduxDispatchProps, OwnProps>(\n    mapStateToProps, mapDispatchToProps)(BaseDeviceGroupPage);\n"
  },
  {
    "path": "ui/_deprecated/DeviceGroupsPage.tsx",
    "content": "import Griddle, {ColumnDefinition, RowDefinition} from \"griddle-react\";\nimport * as React from \"react\";\nimport {connect, Dispatch} from \"react-redux\";\nimport {Link} from \"react-router-dom\";\nimport {bindActionCreators} from \"redux\";\nimport {RootState} from \"../src/reducers/index\";\n\nimport Grid from \"semantic-ui-react/src/collections/Grid\";\nimport Button from \"semantic-ui-react/src/elements/Button\";\nimport Container from \"semantic-ui-react/src/elements/Container\";\nimport Header from \"semantic-ui-react/src/elements/Header\";\n\nimport {RouteComponentProps} from \"react-router\";\nimport {index, IndexActionRequest} from \"../src/actions/device_groups\";\nimport {RouteLinkColumn} from \"../src/components/griddle/RouteLinkColumn\";\nimport {SimpleLayout} from \"../src/components/griddle/SimpleLayout\";\nimport {SelectionPlugin} from \"../src/griddle-plugins/selection/index\";\nimport {SemanticUIPlugin} from \"../src/griddle-plugins/semantic-ui/index\";\nimport {griddle, GriddleDecoratorState} from \"../src/hoc/griddle\";\nimport {DeviceGroupsState} from \"../src/reducers/device_groups\";\n\ninterface ReduxStateProps {\n    device_groups: DeviceGroupsState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): ReduxStateProps {\n    return {\n        device_groups: state.device_groups,\n    };\n}\n\ninterface ReduxDispatchProps {\n    index: IndexActionRequest;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch<RootState>): ReduxDispatchProps {\n    return bindActionCreators({\n        index,\n    }, dispatch);\n}\n\ninterface DeviceGroupsPageProps extends ReduxStateProps, ReduxDispatchProps, RouteComponentProps<void> {\n    griddleState: GriddleDecoratorState;\n    events: any;\n}\n\ninterface DeviceGroupsPageState {\n\n}\n\nclass UnconnectedDeviceGroupsPage extends React.Component<DeviceGroupsPageProps, DeviceGroupsPageState> {\n\n    componentWillMount?() {\n        this.props.index();\n    }\n\n    render() {\n        const {\n            device_groups,\n            griddleState,\n        } = this.props;\n\n        return (\n            <Container className=\"DeviceGroupsPage\">\n                <Grid>\n                    <Grid.Column>\n                        <Header as=\"h1\">Groups</Header>\n                        <Button primary as={Link} to=\"/device_groups/add\">New</Button>\n\n                        <Griddle\n                            data={device_groups.items}\n                            pageProperties={{\n                                currentPage: griddleState.currentPage,\n                                pageSize: griddleState.pageSize,\n                                recordCount: device_groups.recordCount,\n                            }}\n                            styleConfig={{\n                                classNames: {\n                                    Table: \"ui celled table\",\n                                    NoResults: \"ui message\",\n                                },\n                            }}\n                            plugins={[SemanticUIPlugin()]}\n                            components={{\n                                Layout: SimpleLayout,\n                            }}\n                        >\n                            <RowDefinition onClick={() => console.log(\"fmeh\")}>\n                                <ColumnDefinition title=\"ID\" id=\"id\" customComponent={RouteLinkColumn} urlPrefix=\"/device_groups/\" />\n                                <ColumnDefinition title=\"Name\" id=\"attributes.name\" />\n                            </RowDefinition>\n                        </Griddle>\n                    </Grid.Column>\n                </Grid>\n            </Container>\n        );\n    }\n}\n\nexport const DeviceGroupsPage = connect<ReduxStateProps, ReduxDispatchProps, DeviceGroupsPageProps>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(griddle(UnconnectedDeviceGroupsPage));\n"
  },
  {
    "path": "ui/_deprecated/InternalCAPage.tsx",
    "content": "import * as React from 'react';\nimport { connect, Dispatch } from 'react-redux';\nimport {RouteComponentProps} from 'react-router';\nimport {\n    IndexActionRequest, index,\n    DeleteCertificateActionRequest, remove\n} from \"../src/actions/certificates\";\nimport * as caActions from '../src/actions/certificates/ca';\nimport {bindActionCreators} from \"redux\";\nimport {CertificateDetail} from '../src/components/_deprecated/CertificateDetail';\nimport {CAState} from \"../src/reducers/certificates/ca\";\nimport {CAConfigurationForm} from '../src/forms/_retired/CAConfigurationForm';\n\ninterface CAPageState {\n    ca: CAState;\n}\n\ninterface CAPageDispatchProps {\n    remove: DeleteCertificateActionRequest;\n    fetchCACertificates: caActions.FetchCACertificatesActionRequest;\n}\n\ninterface CAPageProps extends CAPageState, CAPageDispatchProps, RouteComponentProps<any> {\n\n}\n\n@connect<CAPageState, CAPageDispatchProps, CAPageProps>(\n    (state: any, ownProps?: any): CAPageState => { return {\n        ca: state.certificates.ca\n    } },\n    (dispatch: Dispatch<any>): CAPageDispatchProps => {\n        return bindActionCreators({\n            fetchCACertificates: caActions.fetchCACertificates,\n            remove\n        }, dispatch);\n    }\n)\nexport class InternalCAPage extends React.Component<CAPageProps, undefined> {\n\n    componentWillMount?() {\n        this.props.fetchCACertificates();\n    }\n\n    handleDeleteCertificate = (certificateId: number): void => {\n        this.props.remove(certificateId);\n    };\n\n    handleDownloadCertificate = (certificateId: number): void => {\n        window.location.href = `/api/v1/certificates/${certificateId}/download`;\n    };\n\n    handleSubmit = (values: FormData): void => {\n\n    };\n\n    render(): JSX.Element {\n        const {\n            ca\n        } = this.props;\n\n        let caCertificate;\n        if (ca && ca.items) {\n            caCertificate = ca.items.data[0];\n        }\n\n        return (\n            <div className='CAPage container top-margin'>\n                <div className='row'>\n                    <div className='column'>\n                        <h1>Internal CA</h1>\n                    </div>\n                </div>\n                <div className='row'>\n                    <div className='column column-25'>\n                        <CertificateDetail\n                            certificate={caCertificate}\n                            title=\"Certificate\"\n                            onClickDelete={this.handleDeleteCertificate}\n                            onClickDownload={this.handleDownloadCertificate}\n                        >\n                        </CertificateDetail>\n                    </div>\n                    <div className='column'>\n                        <CAConfigurationForm onSubmit={this.handleSubmit} />\n                    </div>\n                </div>\n                <div className='row'>\n                    <div className='column top-margin'>\n                        <h2>Issued certificates</h2>\n                    </div>\n                </div>\n            </div>\n        )\n    }\n\n}"
  },
  {
    "path": "ui/_deprecated/MDMPage.tsx",
    "content": "import * as React from 'react';\nimport { connect, Dispatch } from 'react-redux';\nimport {RouteComponentProps} from 'react-router';\nimport { MDMConfigurationForm, FormData } from '../src/forms/_retired/MDMConfigurationForm';\nimport {FetchCertificateTypeActionRequest, fetchCertificatesForType} from \"../src/actions/certificates\";\nimport {bindActionCreators} from \"redux\";\nimport {JSONAPIDetailResponse} from \"../src/json-api\";\nimport {Certificate} from \"../src/store/certificates/types\";\n\n\ninterface MDMPageState {\n    byType?: {[propName: string]: JSONAPIDetailResponse<Certificate, undefined>};\n}\n\ninterface MDMPageDispatchProps {\n    fetchCertificatesForType: FetchCertificateTypeActionRequest;\n}\n\ninterface MDMPageProps extends MDMPageState, MDMPageDispatchProps, RouteComponentProps<any> {\n    \n}\n\n@connect<MDMPageState, MDMPageDispatchProps, MDMPageProps>(\n    (state: any, ownProps?: any): MDMPageState => { return {\n        byType: state.certificates.byType || null\n    } },\n    (dispatch: Dispatch<any>): MDMPageDispatchProps => {\n        return bindActionCreators({\n            fetchCertificatesForType\n        }, dispatch);\n    }\n)\nexport class MDMPage extends React.Component<MDMPageProps & RouteComponentProps<any>, MDMPageState> {\n\n    componentWillMount?(): void {\n        this.props.fetchCertificatesForType('mdm.pushcert');\n        this.props.fetchCertificatesForType('mdm.cacrt');\n    }\n\n    handleSubmit = (values: FormData) => {\n\n    };\n\n    render() {\n        const {\n            byType\n        } = this.props;\n\n        const pushCertificate = byType['mdm.pushcert'];\n        const CACertificate = byType['mdm.cacrt'];\n\n        return (\n            <div className='MDMPage container top-margin'>\n                <div className='row'>\n                    <div className='column'>\n                        <h1>MDM Configuration</h1>\n                        <MDMConfigurationForm\n                            onSubmit={this.handleSubmit}\n                            PushCertificate={pushCertificate}\n                            CACertificate={CACertificate}\n                        />\n                    </div>\n                </div>\n            </div>\n        )\n    }\n\n}"
  },
  {
    "path": "ui/_deprecated/SCEPConfigurationForm.tsx",
    "content": "import * as React from 'react';\nimport {Field, reduxForm, FormProps} from 'redux-form';\nimport {Header, Icon, Segment, Message, Input, Button, Grid, Form} from 'semantic-ui-react';\nimport {SemanticInput} from \"../src/forms/fields/SemanticInput\";\n\n\nexport interface FormData {\n    scep_type: 'internal' | 'external';\n    scep_url: string;\n    scep_challenge: string;\n    scep_subject: string;\n\n}\n\ninterface SCEPConfigurationFormProps extends FormProps<FormData, any, any> {\n\n}\n\n@reduxForm<FormData, SCEPConfigurationFormProps, undefined>({\n    form: 'scep_configuration'\n})\nexport class SCEPConfigurationForm extends React.Component<SCEPConfigurationFormProps, undefined> {\n    render() {\n        const {\n            handleSubmit\n        } = this.props;\n\n        return (\n            <Form onSubmit={handleSubmit}>\n                <Segment>\n                    <label>\n                        <Field name='scep_type' component='input' type='radio' value='internal'/>\n                        <span className=\"label-inline\">Use internal SCEP service</span>\n                    </label>\n                    <p>Your devices will contact this server directly to request their identity certificate.\n                    Use this if you are testing or developing commandment.</p>\n                    \n                </Segment>\n                <Segment>\n                    <label>\n                        <Field name='scep_type' component='input' type='radio' value='external'/>\n                        <span className=\"label-inline\">Use external SCEP service</span>\n                    </label>\n                    <p>Devices will contact an external service to request their identity certificate.</p>\n\n                    <div className='row'>\n                        <div className='column'>\n                            <label htmlFor='scepUrl'>URL</label>\n                            <Field id='scepUrl' name='scep_url' component='input' type='text'\n                                   placeholder='eg. http://scep.example.com/scep'/>\n                            <button className=\"button button-outline\">Test</button>\n                        </div>\n                    </div>\n                    <div className='row'>\n                        <div className='column'>\n                            <label htmlFor='scepChallenge'>Challenge</label>\n                            <Field id='scepChallenge' name='scep_challenge' component='input' type='password'/>\n                        </div>\n                        <div className='column'>\n                            <label htmlFor='scepChallengeConfirm'>Challenge (Confirm)</label>\n                            <Field id='scepChallengeConfirm' name='scep_challenge_confirm' component='input' type='password'/>\n                        </div>\n                    </div>\n\n\n\n                    <label htmlFor='scepSubject'>Request Subject</label>\n                    <Field id='scepSubject' name='scep_subject' component='input' type='text' placeholder='O=Commandment/OU=IT/CN=%HardwareUUID%'/>\n\n                    <input className=\"button-primary\" type=\"submit\" value=\"Save\"/>\n                </Segment>\n            </Form>\n        )\n    }\n}"
  },
  {
    "path": "ui/_deprecated/SSLPage.tsx",
    "content": "import * as React from 'react';\nimport { connect, Dispatch } from 'react-redux';\nimport {RouteComponentProps} from 'react-router';\nimport {\n    IndexActionRequest, index,\n    DeleteCertificateActionRequest, remove\n} from \"../src/actions/certificates\";\nimport * as pushActions from '../src/actions/certificates/push';\nimport * as sslActions from '../src/actions/certificates/ssl';\nimport {bindActionCreators} from \"redux\";\nimport {CertificateDetail} from '../src/components/_deprecated/CertificateDetail';\nimport * as Upload from 'rc-upload';\nimport {PushState} from \"../src/reducers/certificates/push\";\nimport {SSLState} from \"../src/reducers/certificates/ssl\";\n\n\n\ninterface SSLPageState {\n    push: PushState;\n    ssl: SSLState;\n}\n\ninterface SSLPageDispatchProps {\n    index: IndexActionRequest;\n    remove: DeleteCertificateActionRequest;\n    fetchPushCertificates: pushActions.FetchPushCertificatesActionRequest;\n    fetchSSLCertificates: sslActions.FetchSSLCertificatesActionRequest;\n}\n\ninterface SSLPageProps extends SSLPageState, SSLPageDispatchProps, RouteComponentProps<any> {\n\n}\n\n@connect<SSLPageState, SSLPageDispatchProps, SSLPageProps>(\n    (state: any, ownProps?: any): SSLPageState => { return {\n        push: state.certificates.push,\n        ssl: state.certificates.ssl\n    } },\n    (dispatch: Dispatch<any>): SSLPageDispatchProps => {\n        return bindActionCreators({\n            index,\n            fetchPushCertificates: pushActions.fetchPushCertificates,\n            fetchSSLCertificates: sslActions.fetchSSLCertificates,\n            remove\n        }, dispatch);\n    }\n)\nexport class SSLPage extends React.Component<SSLPageProps, undefined> {\n\n    componentWillMount?() {\n        this.props.fetchPushCertificates();\n        this.props.fetchSSLCertificates();\n    }\n\n    handleDeleteCertificate = (certificateId: number): void => {\n        this.props.remove(certificateId);\n    };\n\n    handleDownloadCertificate = (certificateId: number): void => {\n        window.location.href = `/api/v1/certificates/${certificateId}/download`;\n    };\n\n    render(): JSX.Element {\n        const {\n            push,\n            ssl\n        } = this.props;\n\n        let pushCertificate;\n        if (push && push.items) {\n            pushCertificate = push.items.data[0];\n        }\n\n        let sslCertificate;\n        if (ssl && ssl.items) {\n            sslCertificate = ssl.items.data[0];\n        }\n\n        return (\n            <div className='SSLPage container top-margin'>\n                <div className='row'>\n                    <div className='column'>\n                        <h1>SSL Configuration</h1>\n                    </div>\n                </div>\n                <div className='row'>\n                    <div className='column'>\n                        <CertificateDetail certificate={pushCertificate} title=\"Push Certificate\" onClickDelete={this.handleDeleteCertificate} onClickDownload={this.handleDownloadCertificate}>\n                            <button className='button button-outline'>\n                                <i className='fa fa-plus' /> Generate Request\n                            </button>\n                            <Upload\n                                name='file'\n                                accept='application/x-pem-file'\n                                action='/api/v1/push/certificate/public'\n                            >\n                            <button className='button button-outline'>\n                                <i className='fa fa-refresh' /> Replace\n                            </button>\n                            </Upload>\n\n                        </CertificateDetail>\n                    </div>\n                    <div className='column'>\n                        <CertificateDetail certificate={sslCertificate} title=\"SSL Certificate\" onClickDelete={this.handleDeleteCertificate} onClickDownload={this.handleDownloadCertificate}>\n                            <button className='button button-outline'>\n                                <i className='fa fa-plus' /> Generate Request\n                            </button>\n                            <Upload\n                                name='file'\n                                accept='application/x-pem-file'\n                                action='/api/v1/ssl_certificate_data'\n                            >\n                            <button className='button button-outline'>\n                                <i className='fa fa-refresh' /> Replace\n                            </button>\n                            </Upload>\n                            <button className='button button-outline'>\n                                <i className='fa fa-download' /> Download\n                            </button>\n                        </CertificateDetail>\n                    </div>\n                    <div className='column'>\n                        <CertificateDetail certificate={{}} title=\"SCEP CA Certificate\" onClickDelete={this.handleDeleteCertificate} onClickDownload={this.handleDownloadCertificate}>\n                            <Upload\n                                name='file'\n                                accept='application/x-pem-file'\n                                action='/api/v1/scepca_certificate_data'\n                            >\n                                <button className='button button-outline'>\n                                    <i className='fa fa-refresh' /> Replace\n                                </button>\n                            </Upload>\n                            <button className='button button-outline'>\n                                <i className='fa fa-download' /> Download\n                            </button>\n                        </CertificateDetail>\n                    </div>\n                </div>\n            </div>\n        )\n    }\n\n}"
  },
  {
    "path": "ui/_deprecated/assistant/APNSConfiguration.tsx",
    "content": "/// <reference path=\"../../src/typings/rc-upload.d.ts\"/>\nimport * as React from 'react';\nimport * as Upload from 'rc-upload';\n\ninterface APNSConfigurationProps {\n\n}\n\nexport class APNSConfiguration extends React.Component<APNSConfigurationProps,undefined> {\n\n    handleReady = (): void => {\n        console.log('ready');\n    };\n\n    handleStart = (): void => {\n        console.log('start');\n    };\n\n    handleError = (): void => {\n        console.log('er');\n    };\n\n    handleSuccess = (): void => {\n        console.log('success');\n    };\n\n    render(): JSX.Element {\n        \n        return (\n            <div className='APNSConfiguration'>\n                <div className='reversed padded title'><i className=\"fa fa-certificate\" /> Push Certificate</div>\n                <div className='top-margin centered container'>\n                    <div className='row'>\n                        <div className='column'>\n                            <p>The MDM requires a Push Certificate to communicate with devices.</p>\n                        </div>\n                    </div>\n                    <div className='row'>\n                        <div className='column'>\n                            <h3>Upload a Push Certificate (PEM)</h3>\n                            <Upload\n                                name='file'\n                                accept='application/x-pem-file'\n                                action='/api/v1/push.pem'\n                                onReady={this.handleReady}\n                                onStart={this.handleStart}\n                                onError={this.handleError}\n                                onSuccess={this.handleSuccess}\n                            >\n                                <button className='button button-outline'>Upload</button>\n                            </Upload>\n                        </div>\n                        <div className='column column-10 text-middle'>\n                            <h3 className='text-middle'>OR</h3>\n                        </div>\n                        <div className='column'>\n                            <h3>Generate CSR</h3>\n                            <button className='button button-outline'>Generate</button>\n                            <p>\n                                a signing request\n                            </p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        )\n    }\n}"
  },
  {
    "path": "ui/_deprecated/assistant/FinalStep.tsx",
    "content": "import * as React from 'react';\n\ninterface FinalStepProps {\n\n}\n\nexport class FinalStep extends React.Component<FinalStepProps,undefined> {\n\n    render() {\n        return (\n            <div className='FinalStep'>\n                <div className='reversed padded title'><i className=\"fa fa-thumbs-up\" /> Success</div>\n                <div className='top-margin container centered'>\n                    <div className='row'>\n                        <div className='column'>\n                            <p>Congratulations, your commandment server is set up!</p>\n\n                            <p>If your devices are not DEP provisioned,\n                            use the link below to download an enrollment profile.</p>\n\n                            <button className='button'>Enroll</button>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        )\n    }\n}"
  },
  {
    "path": "ui/_deprecated/assistant/SCEPConfiguration.tsx",
    "content": "import * as React from 'react';\nimport {SCEPConfigurationForm} from '../SCEPConfigurationForm';\n\ninterface SCEPConfigurationProps {\n\n}\n\nexport class SCEPConfiguration extends React.Component<SCEPConfigurationProps,undefined> {\n\n    handleLoaded = (loaded: { urls: Array<string>, target: any }) => {\n        console.dir(loaded.urls);\n    };\n\n    handleError = (err: Error) => {\n        console.log(err);\n    };\n\n\n    render() {\n        return (\n            <div className='SCEPConfiguration'>\n                <div className='reversed padded title'><i className=\"fa fa-mobile\" /> SCEP Configuration</div>\n                <div className='top-margin container'>\n                    <div className='row'>\n                        <div className='column'>\n                            <p>Devices need to request identity certificates to prove that they are enrolled in your MDM.\n                            This is done through a SCEP service. You can use the built-in service for testing, or\n                            provide information about your production SCEP server.</p>\n                        </div>\n                    </div>\n                    <div className='row'>\n                        <div className='column'>\n                            <SCEPConfigurationForm />\n                        </div>\n                    </div>\n                </div>\n            </div>\n        )\n    }\n}"
  },
  {
    "path": "ui/_deprecated/assistant/SSLConfiguration.tsx",
    "content": "import * as React from 'react';\nimport * as Upload from 'rc-upload';\n\ninterface SSLConfigurationProps {\n    onClickGenerateCSR: () => void;\n    SSLSigningRequest?: any;\n}\n\nexport class SSLConfiguration extends React.Component<SSLConfigurationProps,undefined> {\n\n    handleLoaded = (urls: Array<string>) => {\n        console.dir(urls);\n    };\n\n    handleError = (err: Error) => {\n        console.log(err);\n    };\n\n    handleGenerateCSR = (event: any): void => {\n        event.preventDefault();\n        this.props.onClickGenerateCSR();\n    };\n\n    render() {\n        const {\n            SSLSigningRequest\n        } = this.props;\n\n        return (\n            <div className='SSLConfiguration'>\n                <div className='reversed padded title'><i className=\"fa fa-certificate\" /> Web Certificate</div>\n                <div className='top-margin centered container'>\n                    <div className='row'>\n                        <div className='column'>\n                            <p>You need to generate an SSL Certificate to encrypt communications between the device and\n                            the MDM</p>\n                        </div>\n                    </div>\n                    <div className='row'>\n                        <div className='column'>\n                            <h3>Upload an SSL Certificate</h3>\n                            <Upload\n                                name='ssl_certificate'\n                                accept=\"application/x-pkcs12\"\n                            />\n                        </div>\n                        <div className='column column-10 text-middle'>\n                            <h3 className='text-middle'>OR</h3>\n                        </div>\n                        <div className='column'>\n                            <button className='button button-outline' onClick={this.handleGenerateCSR}>Generate</button>\n                            <p>\n                                a signing request\n                            </p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        )\n    }\n}"
  },
  {
    "path": "ui/babel.config.js",
    "content": "module.exports = function(api) {\n  api.cache(true);\n\n  return {\n    plugins: [\n      \"@babel/plugin-proposal-export-default-from\",\n      \"@babel/plugin-syntax-jsx\",\n      \"@babel/plugin-transform-react-jsx\",\n      \"@babel/plugin-transform-react-display-name\",\n      \"@babel/plugin-proposal-class-properties\",\n      \"@babel/plugin-proposal-export-namespace-from\",\n      \"react-hot-loader/babel\"\n    ],\n    presets: [\n\n    ],\n  };\n};"
  },
  {
    "path": "ui/package.json",
    "content": "{\n  \"name\": \"commandment-ui\",\n  \"version\": \"1.0.0\",\n  \"description\": \"UI for commandment\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"./node_modules/.bin/webpack-dev-server\",\n    \"storybook\": \"start-storybook -p 6006 -c .storybook\",\n    \"build-storybook\": \"build-storybook\",\n    \"lint\": \"eslint . --ext .ts,.tsx\",\n    \"lint-fix\": \"eslint . --ext .ts,.tsx --fix\"\n  },\n  \"author\": \"mosen\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"devDependencies\": {\n    \"@storybook/addon-actions\": \"^5.0.11\",\n    \"@storybook/addons\": \"^5.0.11\",\n    \"@storybook/react\": \"^5.0.11\",\n    \"@typescript-eslint/eslint-plugin\": \"^1.9.0\",\n    \"@typescript-eslint/parser\": \"^1.9.0\",\n    \"babel-runtime\": \"^6.26.0\",\n    \"eslint\": \"^5.16.0\",\n    \"eslint-plugin-react\": \"^7.13.0\",\n    \"webpack-bundle-analyzer\": \"^3.3.2\",\n    \"webpack-cli\": \"^3.1.2\",\n    \"webpack-dev-server\": \"^3.1.4\",\n    \"webpack-hot-middleware\": \"^2.18.2\"\n  },\n  \"dependencies\": {\n    \"@babel/core\": \"^7.1.6\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.1.0\",\n    \"@babel/plugin-proposal-export-default-from\": \"^7.0.0\",\n    \"@babel/plugin-proposal-export-namespace-from\": \"^7.0.0\",\n    \"@babel/plugin-syntax-jsx\": \"^7.0.0\",\n    \"@babel/plugin-transform-react-display-name\": \"^7.0.0\",\n    \"@babel/plugin-transform-react-jsx\": \"^7.1.6\",\n    \"@types/fetch-jsonp\": \"^1.0.0\",\n    \"@types/history\": \"^4.6.0\",\n    \"@types/lodash\": \"^4.14.119\",\n    \"@types/lodash-es\": \"^4.17.0\",\n    \"@types/prop-types\": \"^15.5.8\",\n    \"@types/react\": \"^16.8.17\",\n    \"@types/react-dom\": \"^16.8.4\",\n    \"@types/react-dropzone\": \"^4.2.2\",\n    \"@types/react-hot-loader\": \"^4.1.0\",\n    \"@types/react-redux\": \"^7.0.9\",\n    \"@types/react-router\": \"^5.0.0\",\n    \"@types/react-router-dom\": \"^4.3.3\",\n    \"@types/react-table\": \"^6.8.3\",\n    \"@types/recompose\": \"^0.30.6\",\n    \"@types/redux-devtools\": \"^3.0.46\",\n    \"@types/semver\": \"^6.0.0\",\n    \"@types/storybook__addon-actions\": \"^3.4.3\",\n    \"@types/storybook__react\": \"^4.0.1\",\n    \"@types/webpack\": \"^4.4.32\",\n    \"@types/webpack-env\": \"^1.13.9\",\n    \"@types/yup\": \"^0.26.1\",\n    \"awesome-typescript-loader\": \"^5.0.0\",\n    \"babel-loader\": \"^8.0.6\",\n    \"babel-plugin-lodash\": \"^3.3.4\",\n    \"babel-preset-env\": \"^1.7.0\",\n    \"byte-size\": \"^5.0.1\",\n    \"connected-react-router\": \"^6.4.0\",\n    \"css-loader\": \"2.1.1\",\n    \"date-fns\": \"^1.29.0\",\n    \"extract-text-webpack-plugin\": \"^4.0.0-beta.0\",\n    \"fetch-jsonp\": \"^1.1.3\",\n    \"file-loader\": \"^3.0.1\",\n    \"font-awesome\": \"^4.7.0\",\n    \"formik\": \"^1.5.7\",\n    \"formik-semantic-ui\": \"^0.9.2\",\n    \"history\": \"^4.7.2\",\n    \"immutable\": \"^4.0.0-rc.12\",\n    \"is-arrayish\": \"^0.3.1\",\n    \"moment\": \"^2.19.1\",\n    \"node-sass\": \"^4.5.1\",\n    \"prop-types\": \"^15.5.10\",\n    \"react\": \"^16.8.6\",\n    \"react-dom\": \"^16.8.6\",\n    \"react-dropzone\": \"^10.1.5\",\n    \"react-hot-loader\": \"^4.2.0\",\n    \"react-redux\": \"^7.0.3\",\n    \"react-router\": \"^5.0.0\",\n    \"react-router-dom\": \"^5.0.0\",\n    \"react-simple-pie-chart\": \"^0.5.0\",\n    \"react-table\": \"^6.9.2\",\n    \"recompose\": \"^0.30.0\",\n    \"redux\": \"^4.0.1\",\n    \"redux-api-middleware\": \"^3.0.1\",\n    \"redux-debounce\": \"^1.0.1\",\n    \"redux-thunk\": \"^2.3.0\",\n    \"reselect\": \"^4.0.0\",\n    \"resolve-url-loader\": \"^3.1.0\",\n    \"sass-loader\": \"^7.1.0\",\n    \"semantic-ui-css\": \"^2.4.1\",\n    \"semantic-ui-react\": \"^0.87.1\",\n    \"semver\": \"^6.0.0\",\n    \"style-loader\": \"^0.23.0\",\n    \"typescript\": \"^3.4.4\",\n    \"typescript-plugin-lodash\": \"^0.1.0\",\n    \"url-loader\": \"^1.1.2\",\n    \"webpack\": \"^4.31.0\",\n    \"yup\": \"^0.27.0\"\n  },\n  \"resolutions\": {\n    \"@types/react\": \"^16.8.17\",\n    \"**/@types/react\": \"^16.8.17\"\n  }\n}\n"
  },
  {
    "path": "ui/sass/_dropzone.scss",
    "content": ".dropzone {\n  margin: 1rem 0;\n  height: 5rem;\n\n  border: 1px solid rgba(34,36,38,.1);\n  background-color: #F9FAFB;\n  text-align: center;\n  cursor: pointer;\n\n  .ui.header {\n    line-height: 5rem;\n  }\n}"
  },
  {
    "path": "ui/sass/_helper.scss",
    "content": "@import 'settings';\n\n.error {\n  font-weight: bold;\n  color: $highlight-color;\n}"
  },
  {
    "path": "ui/sass/_nav.scss",
    "content": "*, *:before, *:after {\n  box-sizing: inherit;\n}\n\n//.navigation {\n//  box-sizing: border-box;\n//  display: flex;\n//  align-items: center;\n//  width: 100%;\n//  background: #eee;\n//  padding: 20px;\n//}\n\nnav {\n  background: #eee;\n}\n\n// top-level menu\nnav ul {\n  margin: 0;\n  list-style: none;\n  position: relative;\n  display: flex;\n  //padding: 1rem 0;  // same as milligram body padding\n\n  li {\n    float: left;\n    margin: 0;  // cancel out the milligram default margin\n\n    a {\n      padding: 1rem 2rem;\n      display: block;\n    }\n\n    span { // if not using a link\n      display: inline-block;\n      padding: 1rem 2rem;\n    }\n  }\n\n  li:hover {\n    // hover style\n  }\n\n  // make submenu active\n  li:hover > ul {\n    display: block;\n  }\n\n  li:active > ul {\n    display: block;\n  }\n}\n\nnav ul:after {\n  content: \"\"; clear: both; display: block;\n}\n\n// submenu\nnav ul ul {\n  z-index: 99;\n  background-color: #9b4dca;\n  position: absolute;\n  top: 100%;\n\n  display: none;\n  padding: 0;\n\n  li {\n    float: none;\n    position: relative;\n\n    a {\n      padding: 1rem 2rem;\n      color: #fff;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/sass/_settings.scss",
    "content": "$highlight-color: #d241c7;\n"
  },
  {
    "path": "ui/sass/_upload.scss",
    "content": ".ap-upload-input {\n  background-color: #eee;\n  border: 1px solid #aaa;\n  border-radius: 0.4rem;\n}"
  },
  {
    "path": "ui/sass/app.scss",
    "content": "@import '~semantic-ui-css/semantic.min.css';\n@import '~font-awesome/scss/font-awesome';\n@import '~react-table/react-table.css';\n@import 'settings';\n@import 'helper';\n@import 'upload';\n@import 'nav';\n@import 'dropzone';\n\n.top-margin {\n  margin-top: 2rem;\n}\n\n/* intended to be used where a button must appear next to an input with a label on top */\n.form-field-button {\n  margin-bottom: 1.5rem;\n}\n\n.paper {\n  box-shadow: rgba(0, 0, 0, 0.117647) 0 1px 6px, rgba(0, 0, 0, 0.117647) 0 1px 4px;\n}\n\n.centered {\n  text-align: center;\n}\n\n.padded {\n  padding: 1.6rem;\n}\n\n.padded-sides {\n  padding: 0 1.6rem;\n}\n\n.reversed {\n  background-color: #9b4dca;\n  color: white;\n}\n\n.title {\n  font-size: 2.4rem;\n}\n\n.text-middle {\n  vertical-align: middle;\n}\n\n.warning {\n  color: red;\n}\n\n\n// TEST: Padding glyphicons in headers\nh3 i {\n  margin-right: 0.8rem;\n}\n\ndl.horizontal {\n\n  dt {\n    width: 20%;\n    float: left;\n    font-weight: bold;\n  }\n  dd {\n    margin: 0;\n  }\n  dd:after {\n    content: '';\n    display: block;\n    clear: both;\n  }\n}\n\n// Padding out icons in lists because they never line up in semantic-ui\n.ui.list > .item > i.icon {\n  min-width: 40px;\n}"
  },
  {
    "path": "ui/src/@types/byte-size/index.d.ts",
    "content": "declare module \"byte-size\" {\n\n    export = byteSize\n\n    function byteSize(bytes: number, options?: byteSize.Options): byteSize.ByteSize;\n    namespace byteSize {\n        export interface Options {\n            precision: number;\n            units: \"metric\" | \"iec\" | \"metric_octet\" | \"iec_octet\"\n        }\n\n        export interface ByteSize {\n            value: string;\n            unit: string;\n            toString(): string;\n        }\n    }\n}\n"
  },
  {
    "path": "ui/src/@types/redux-api-middleware/index.d.ts",
    "content": "declare module \"redux-api-middleware\" {\n    import {Action, AnyAction, Middleware} from \"redux\";\n    /**\n     * Symbol key that carries API call info interpreted by this Redux middleware.\n     *\n     * @constant {string}\n     * @access public\n     * @default\n     */\n    export const RSAA: string;\n    export type RSAA = \"@@redux-api-middleware/RSAA\";\n\n//// ERRORS\n\n    export enum ErrorNames {\n        ApiError = \"ApiError\",\n        InternalError = \"InternalError\",\n        InvalidRSAA = \"InvalidRSAA\",\n        RequestError = \"RequestError\",\n    }\n\n    /**\n     * Error class for an RSAA that does not conform to the RSAA definition\n     *\n     * @class InvalidRSAA\n     * @access public\n     * @param {array} validationErrors - an array of validation errors\n     */\n    export class InvalidRSAA extends Error {\n        public name: ErrorNames.InvalidRSAA;\n        public message: string;\n        public validationErrors: string[];\n\n        constructor(validationErrors: string[]);\n    }\n\n    /**\n     * Error class for a custom `payload` or `meta` function throwing\n     *\n     * @class InternalError\n     * @access public\n     * @param {string} message - the error message\n     */\n    export class InternalError extends Error {\n        public name: ErrorNames.InternalError;\n        public message: string;\n\n        constructor(message: string);\n    }\n\n    /**\n     * Error class for an error raised trying to make an API call\n     *\n     * @class RequestError\n     * @access public\n     * @param {string} message - the error message\n     */\n    export class RequestError extends Error {\n        public name: ErrorNames.RequestError;\n        public message: string;\n\n        constructor(message: string);\n    }\n\n    /**\n     * Error class for an API response outside the 200 range\n     *\n     * @class ApiError\n     * @access public\n     * @param {number} status - the status code of the API response\n     * @param {string} statusText - the status text of the API response\n     * @param {object} response - the parsed JSON response of the API server if the\n     *  'Content-Type' header signals a JSON response\n     */\n    export class ApiError<R = any> extends Error {\n        public name: ErrorNames.ApiError;\n        public message: string;\n        public status: number;\n        public statusText: string;\n        public response?: R;\n\n        constructor(status: number, statusText: string, response: R);\n    }\n\n//// VALIDATION\n\n    /**\n     * Is the given action a plain JavaScript object with a [RSAA] property?\n     */\n    export function isRSAA(action: any): action is RSAAction<any, any, any>;\n\n    /**\n     * The README explains the following criteria for a TypeDescriptor:\n     *\n     * A type descriptor **MUST**:\n     * - be a plain JavaScript object\n     * - have a `type` property, which **MUST** be a string or a `Symbol`.\n     */\n    export interface TypeDescriptor<TSymbol, TPayload = any, TMeta = any> {\n        type: string | TSymbol;\n        payload?: TPayload;\n        meta?: TMeta;\n    }\n\n    /**\n     * Is the given object a valid type descriptor?\n     */\n    export function isValidTypeDescriptor(obj: object): obj is TypeDescriptor<any>;\n\n    /**\n     * Checks an action against the RSAA definition, returning a (possibly empty)\n     * array of validation errors.\n     */\n    export function validateRSAA(action: any): string[];\n\n    /**\n     * Is the given action a valid RSAA?\n     */\n    export function isValidRSAA(action: any): boolean;\n\n//// MIDDLEWARE\n\n    export interface MiddlewareOptions {\n        // Determines whether the response is an error\n        ok: (res: any) => boolean;\n        fetch: GlobalFetch;\n    }\n\n    /**\n     * Create middleware with custom options.\n     */\n    export function createMiddleware(options?: MiddlewareOptions): Middleware;\n\n    /**\n     * A Redux middleware that processes RSAA actions.\n     */\n    export const apiMiddleware: Middleware;\n\n//// UTIL\n\n    /**\n     * Extract JSON body from a server response\n     */\n    export function getJSON(res: Response): PromiseLike<any> | undefined;\n\n    export type RSAActionTypeTuple = [string | symbol, string | symbol, string | symbol];\n\n    /**\n     * Blow up string or symbol types into full-fledged type descriptors,\n     *   and add defaults\n     */\n    export function normalizeTypeDescriptors(types: RSAActionTypeTuple): RSAActionTypeTuple;\n\n    export type HTTPVerb = \"GET\" | \"HEAD\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\";\n\n    export interface RSAActionBody<R, S, F> {\n        endpoint: string;  // or function\n        method: HTTPVerb;\n        body?: any;\n        headers?: { [propName: string]: string }; // or function\n        credentials?: \"omit\" | \"same-origin\" | \"include\";\n        bailout?: boolean; // or function\n        types: [R, S, F];\n    }\n\n    export enum Credentials {\n        omit = \"omit\",\n        sameOrigin = \"same-origjn\",\n        include = \"include\",\n    }\n\n    export type RSAAActionType = string | TypeDescriptor<any>;\n    export type RSAAActionTypes = [RSAAActionType, RSAAActionType, RSAAActionType];\n\n    export interface RSAAction<TRequest, TSuccess, TFail> {\n        [propName: string]: { // Symbol as object key seems impossible\n            endpoint: string;  // or function\n            method: HTTPVerb;\n            types: [TRequest, TSuccess, TFail];\n            body?: any;\n            headers?: any; // or function\n            options?: any;\n            credentials?: Credentials;\n            bailout?: boolean; // or function\n            fetch?: GlobalFetch;\n            ok?: any;\n        }\n    }\n\n    //// Augmentations\n\n    module \"redux\" {\n        export interface AnyAction {\n            \"@@redux-api-middleware/RSAA\"?: RSAActionBody<any, any, any>;\n        }\n    }\n}\n"
  },
  {
    "path": "ui/src/components/ActionMenu.tsx",
    "content": "import * as React from \"react\";\nimport {Dropdown} from \"semantic-ui-react\";\n\nexport enum UIActionTypes {\n    BLANK_PUSH = \"BLANK_PUSH\",\n    CLEAR_PASSCODE = \"CLEAR_PASSCODE\",\n    FULL_INVENTORY = \"FULL_INVENTORY\",\n}\n\nexport interface IActionMenu {\n    enabledActions: UIActionTypes[];\n}\n\nexport const ActionMenu: React.FunctionComponent<IActionMenu> = (props: IActionMenu) => (\n    <Dropdown inline button text=\"action\" onChange={this.handleAction} options={[\n        {text: \"Force Push\", value: \"push\"},\n        {text: \"Inventory\", value: \"inventory\"},\n        {text: \"Test\", value: \"test\"},\n    ]}></Dropdown>\n);\n"
  },
  {
    "path": "ui/src/components/App.tsx",
    "content": "import * as React from \"react\";\nimport {hot} from \"react-hot-loader\";\n\n/**\n * AppLayout is the top level root display component.\n *\n * It is recommended to keep this as a class and not a stateless component, due to earlier issues with react-router not\n * updating children.\n *\n * It is also recommended to keep this as an unconnected component for the same reason.\n *\n * @see https://github.com/ReactTraining/react-router/issues/4975\n */\nclass AppCool extends React.Component<{}, {}> {\n    public render() {\n        const {children} = this.props;\n\n        return (\n          <div className=\"App\">\n              {children}\n          </div>\n        );\n    }\n}\n\nexport const App = hot(module)(AppCool);\n"
  },
  {
    "path": "ui/src/components/BareLayout.tsx",
    "content": "import * as React from \"react\";\n\nimport {Grid} from \"semantic-ui-react\";\nimport {Route, RouteProps} from \"react-router\";\nimport {ComponentClass, FunctionComponent} from \"react\";\n\ninterface INavigationLayout {\n    component: ComponentClass;\n}\n\nexport const BareLayout: FunctionComponent<INavigationLayout & RouteProps> = ({ Component: component, ...rest }) => (\n    <Route {...rest} render={matchProps => (\n        <Grid className=\"BareLayout\">\n            <Component {...matchProps} />\n        </Grid>\n    )} />\n);\n"
  },
  {
    "path": "ui/src/components/CertificateTypeIcon.tsx",
    "content": "import * as React from \"react\";\nimport {Icon} from \"semantic-ui-react\";\n\ninterface CertificateTypeIconProps {\n    value: number;\n    title: string;\n}\n\nexport const CertificateTypeIcon: React.StatelessComponent<CertificateTypeIconProps> = (props: CertificateTypeIconProps): JSX.Element => {\n    return <Icon name={props.value ? \"id badge\" : \"certificate\"} />;\n};\n"
  },
  {
    "path": "ui/src/components/CheckListItem.tsx",
    "content": "import * as React from \"react\";\nimport {List} from \"semantic-ui-react\";\n\ninterface ICheckListItemProps {\n    title: string;\n    description?: string;\n    value: any; // will be interpreted as boolean\n    children?: JSX.Element[] | JSX.Element;\n}\n\nexport const CheckListItem: React.FunctionComponent<ICheckListItemProps> = ({ title, value, description, children }: ICheckListItemProps) => (\n    <List.Item>\n        {value ? <List.Icon name=\"checkmark\" size=\"large\" /> : <List.Icon name=\"remove\" size=\"large\" />}\n        <List.Content>\n            <List.Header>{title}</List.Header>\n            {children &&\n                <List>\n                    {children}\n                </List>\n            }\n        </List.Content>\n    </List.Item>\n);\n"
  },
  {
    "path": "ui/src/components/DeviceActions.tsx",
    "content": "import * as React from 'react';\n\ninterface DeviceActionsProps {\n\n}\n\nexport const DeviceActions: React.StatelessComponent<DeviceActionsProps> = (props: DeviceActionsProps) => (\n    <div></div>\n);"
  },
  {
    "path": "ui/src/components/Navigation.scss",
    "content": ""
  },
  {
    "path": "ui/src/components/Navigation.tsx",
    "content": "import * as React from \"react\";\nimport {Menu} from \"semantic-ui-react\";\n\nimport {MenuItemLink} from \"../components/semantic-ui/MenuItemLink\";\nimport \"./Navigation.scss\";\n\nexport interface INavigationProps {\n\n}\n\nexport const Navigation: React.StatelessComponent<INavigationProps> = (props: INavigationProps) => (\n    <Menu>\n        <MenuItemLink header to=\"/\" activeOnlyWhenExact>CMDMNT</MenuItemLink>\n        <MenuItemLink to=\"/devices\">Devices</MenuItemLink>\n        <MenuItemLink to=\"/profiles\">Profiles</MenuItemLink>\n        <MenuItemLink to=\"/applications\">Applications</MenuItemLink>\n        <MenuItemLink to=\"/settings\">Settings</MenuItemLink>\n    </Menu>\n);\n"
  },
  {
    "path": "ui/src/components/NavigationLayout.tsx",
    "content": "import * as React from \"react\";\n\nimport {Grid} from \"semantic-ui-react\";\nimport {NavigationVertical} from \"./NavigationVertical\";\nimport {RouteComponentProps} from \"react-router\";\nimport {ComponentProps, FunctionComponent} from \"react\";\n\nexport const NavigationLayout: FunctionComponent<RouteComponentProps> = (props: RouteComponentProps & ComponentProps) => (\n    <Grid className=\"NavigationLayout\">\n        <Grid.Column width={2}>\n            <NavigationVertical/>\n        </Grid.Column>\n        <Grid.Column width={12}>\n            {props.children}\n        </Grid.Column>\n    </Grid>\n);\n"
  },
  {
    "path": "ui/src/components/NavigationVertical.tsx",
    "content": "import * as React from \"react\";\n\nimport {MenuItemLink} from \"../components/semantic-ui/MenuItemLink\";\nimport \"./Navigation.scss\";\nimport {RouteComponentProps} from \"react-router\";\nimport {Divider, Sidebar, Menu} from \"semantic-ui-react\";\n\ninterface IRouteProps {\n}\n\nexport interface INavigationVerticalProps extends RouteComponentProps<IRouteProps> {\n\n}\n\nexport const NavigationVertical: React.FC<INavigationVerticalProps> = (props: INavigationVerticalProps) => (\n    <Sidebar as={Menu} secondary vertical visible>\n            <MenuItemLink header to=\"/\" activeOnlyWhenExact>CMDMNT</MenuItemLink>\n            <MenuItemLink to=\"/devices\">Devices</MenuItemLink>\n            <MenuItemLink to=\"/profiles\">Profiles</MenuItemLink>\n            <MenuItemLink to=\"/applications\">Applications</MenuItemLink>\n            <MenuItemLink to=\"/settings\">Settings</MenuItemLink>\n            <Divider />\n            <MenuItemLink to=\"/logout\">Logout</MenuItemLink>\n    </Sidebar>\n);\n"
  },
  {
    "path": "ui/src/components/ProtectedRoute.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {FunctionComponent, Component} from \"react\";\nimport {Redirect, Route} from \"react-router\";\nimport {RootState} from \"../reducers\";\n\nexport interface IProtectedRoute {\n    component: Component;\n    access_token: string;\n}\n\nconst UnconnectedProtectedRoute: FunctionComponent =\n    ({component: Component, access_token, ...rest}: Partial<IProtectedRoute>) => (\n\n    <Route\n        {...rest}\n        render={props => access_token ? (\n            <Component {...props} />\n        ) : (\n            <Redirect to={{\n                pathname: \"/login\",\n                state: {from: props.location}\n            }}/>\n        )}\n    />\n);\n\nexport const ProtectedRoute = connect((state: RootState) => {\n    return {\n        access_token: state.auth.access_token,\n        expires_in: state.auth.expires_in,\n    }\n}, null)(UnconnectedProtectedRoute);\n"
  },
  {
    "path": "ui/src/components/RSAAApiErrorMessage.tsx",
    "content": "import * as React from \"react\";\nimport {ApiError} from \"redux-api-middleware\";\nimport {Message} from \"semantic-ui-react\";\nimport {JSONAPIErrorObject, JSONAPIErrorResponse} from \"../store/json-api\";\n\nexport interface IRSAAApiErrorMessageProps {\n    error: ApiError<any>;\n}\n\nexport const RSAAApiErrorMessage: React.FunctionComponent<IRSAAApiErrorMessageProps> =\n    (props: IRSAAApiErrorMessageProps) => (\n    <Message\n        error\n        header=\"An error occurred communicating with the server\"\n        list={[\n            `Status: ${props.error.status} - ${props.error.statusText}`,\n            // ...props.error.response.errors.map((err: JSONAPIErrorObject) => `${err.detail}`),\n        ]}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/SearchInput.tsx",
    "content": "import * as React from \"react\";\nimport {Input, InputOnChangeData, InputProps} from \"semantic-ui-react\";\nimport Timeout = NodeJS.Timeout;\n\nexport interface ISearchInputProps {\n    duration: number;\n    loading: boolean;\n    onSearch: (value: string) => void;\n}\n\nexport interface ISearchInputState {\n    value: string;\n    timeout: Timeout;\n}\n\nexport class SearchInput extends React.Component<ISearchInputProps, ISearchInputState> {\n\n    // public static initialState: ISearchInputState = {\n    //     timeout: null,\n    //     value: \"\",\n    // };\n    constructor(props: ISearchInputProps) {\n        super(props);\n        this.state = { timeout: null, value: \"\" };\n    }\n\n    public render() {\n        const { loading } = this.props;\n\n        return (\n            <Input loading={loading ? true : undefined}\n                   icon=\"search\"\n                   placeholder=\"App Name...\"\n                   onChange={this.handleChange}\n                   value={this.state.value}\n            />\n        )\n    }\n\n    private handleTimeout = (e: any) => {\n        this.props.onSearch(this.state.value);\n        this.setState({ timeout: null });\n    };\n\n    private handleChange = (event: any, data: InputOnChangeData) => {\n        if (this.state.timeout) {\n            clearTimeout(this.state.timeout);\n        }\n\n        const timeout = setTimeout(this.handleTimeout, this.props.duration | 400);\n        this.setState({ timeout, value: data.value });\n    };\n}\n"
  },
  {
    "path": "ui/src/components/TagDropdown.tsx",
    "content": "import * as React from \"react\";\nimport {SyntheticEvent} from \"react\";\nimport {JSONAPIDataObject} from \"../store/json-api\";\nimport {Tag} from \"../store/tags/types\";\n\nimport { Dropdown, DropdownProps } from \"semantic-ui-react\";\n\n// Not exported by Dropdown\ninterface IDropdownOnSearchChangeData extends DropdownProps {\n    searchQuery: string;\n}\n\ninterface ITagDropdownProps {\n    loading: boolean;\n    tags: Array<JSONAPIDataObject<Tag>>;\n    value?: any[];\n    onAddItem: (event: SyntheticEvent<any>, data: object) => void;\n    onSearch: (value: string) => void;\n    onChange: (event: SyntheticEvent<any>, values: string[]) => void;\n    searchTimeout: number;\n}\n\ninterface ITagDropdownState {\n    value?: string;\n}\n\nexport class TagDropdown extends React.Component<ITagDropdownProps, ITagDropdownState> {\n\n    private timeout: number;\n\n    constructor(props: ITagDropdownProps) {\n        super(props);\n        this.state = {\n            value: \"\",\n        };\n    }\n\n    public render() {\n        const { tags, loading, onAddItem, value, onChange } = this.props;\n\n        const options = tags.map((item: JSONAPIDataObject<Tag>) => {\n            return {\n                key: item.id,\n                label: { color: item.attributes.color, empty: true, circular: true },\n                text: item.attributes.name,\n                value: item.id,\n            };\n        });\n\n        return (\n            <Dropdown placeholder=\"Add Tag(s)\"\n                      multiple\n                      allowAdditions\n                      additionLabel=\"Create new tag \"\n                      search\n                      selection\n                      loading={loading}\n                      options={options}\n                      onAddItem={onAddItem}\n                      onSearchChange={this.handleSearchChange}\n                      onChange={onChange}\n                      value={value}\n            />\n        );\n    }\n\n    private performSearch = () => {\n        console.log(\"perform search\");\n        this.props.onSearch(this.state.value);\n    };\n\n    private handleSearchChange = (event: React.SyntheticEvent<HTMLElement>, data: IDropdownOnSearchChangeData): void => {\n        console.log(\"change\");\n        if (this.timeout) { clearTimeout(this.timeout); }\n        this.setState({ value: data.searchQuery });\n\n        if (data.length > 0) {\n            this.timeout = window.setTimeout(this.performSearch, 400);\n        }\n    };\n}\n"
  },
  {
    "path": "ui/src/components/devices/DEPDeviceDetail.tsx",
    "content": "import {FunctionComponent} from \"react\";\nimport {DeviceState} from \"../../store/device/reducer\";\nimport * as React from \"react\";\nimport {format} from \"date-fns\";\nimport {\n    Divider,\n    Grid,\n    Button,\n    Header,\n    List,\n    Message\n} from \"semantic-ui-react\";\n\nexport interface IDEPDeviceDetailProps {\n    device: DeviceState;\n}\n\nexport const DEPDeviceDetail: FunctionComponent<IDEPDeviceDetailProps> =\n    ({device, ...props}: IDEPDeviceDetailProps) => {\n\n    return (\n        <div className=\"DEPDeviceDetail\">\n            <Divider hidden />\n            <Header as=\"h1\">\n                {device.device.attributes.description}\n                <Header.Subheader>SN: {device.device.attributes.serial_number}</Header.Subheader>\n            </Header>\n\n            <Message>DEP Device - Not yet enrolled</Message>\n\n            <Grid columns={2}>\n                <Grid.Row>\n                    <Grid.Column>\n                        <List>\n                            <List.Item>\n                                <List.Icon name=\"cube\" size=\"large\" verticalAlign=\"middle\" />\n                                <List.Content>\n                                    <List.Header>Model</List.Header>\n                                    <List.Description>{device.device.attributes.model}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"paint brush\" size=\"large\" verticalAlign=\"middle\" />\n                                <List.Content>\n                                    <List.Header>Colour</List.Header>\n                                    <List.Description>{device.device.attributes.color}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                        </List>\n                    </Grid.Column>\n                    <Grid.Column>\n                        <List>\n                            <List.Item>\n                                <List.Icon name=\"calendar\" size=\"large\" verticalAlign=\"middle\" />\n                                <List.Content>\n                                    <List.Header>Assigned to this MDM</List.Header>\n                                    <List.Description>{format(device.device.attributes.device_assigned_date)}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"user\" size=\"large\" verticalAlign=\"middle\" />\n                                <List.Content>\n                                    <List.Header>Assigned by Apple ID</List.Header>\n                                    <List.Description>{device.device.attributes.device_assigned_by}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"cloud\" size=\"large\" verticalAlign=\"middle\" />\n                                <List.Content>\n                                    <List.Header>Profile Status</List.Header>\n                                    <List.Description>{device.device.attributes.profile_status}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                        </List>\n                    </Grid.Column>\n                </Grid.Row>\n            </Grid>\n\n            <Button>Assign DEP Profile</Button>\n        </div>\n    );\n};\n"
  },
  {
    "path": "ui/src/components/devices/IOSDeviceDetail.tsx",
    "content": "import {distanceInWordsToNow} from \"date-fns\";\nimport * as React from \"react\";\nimport {DeviceState} from \"../../store/device/reducer\";\n\nimport {Route} from \"react-router\";\nimport {DeviceRename} from \"../../containers/DeviceRename\";\nimport {DeviceApplications} from \"../../containers/devices/DeviceApplications\";\nimport {DeviceCertificates} from \"../../containers/devices/DeviceCertificates\";\nimport {DeviceCommands} from \"../../containers/devices/DeviceCommands\";\nimport {DeviceDetail} from \"../../containers/devices/DeviceDetail\";\nimport {DeviceOSUpdates} from \"../../containers/devices/DeviceOSUpdates\";\nimport {DeviceProfiles} from \"../../containers/devices/DeviceProfiles\";\nimport {\n    ClearPasscodeActionRequest, InventoryActionRequest, LockActionRequest,\n    PushActionRequest,\n    RestartActionRequest,\n    ShutdownActionRequest,\n    TestActionRequest,\n} from \"../../store/device/actions\";\nimport {ButtonLink} from \"../semantic-ui/ButtonLink\";\nimport {MenuItemLink} from \"../semantic-ui/MenuItemLink\";\nimport {TagDropdown} from \"../TagDropdown\";\n\nimport {\n    Divider,\n    Icon,\n    Segment,\n    Grid,\n    Menu,\n    Button,\n    Header,\n    List,\n    DropdownProps,\n    DropdownItemProps,\n} from \"semantic-ui-react\";\n\nimport {SyntheticEvent} from \"react\";\nimport {ITagsState} from \"../../store/tags/reducer\";\nimport \"./MacOSDeviceDetail.scss\";\n\ninterface IIOSDeviceDetailProps {\n    device: DeviceState;\n    tags: ITagsState;\n    deviceTags: number[];\n\n    onAddTag: (event: SyntheticEvent<any>, data: object) => void;\n    onChangeTag: (event: SyntheticEvent<HTMLElement>, data: DropdownProps) => void;\n    onSearchTag: (value: string) => void;\n\n    clearPasscode: ClearPasscodeActionRequest;\n    inventory: InventoryActionRequest;\n    lock: LockActionRequest;\n    push: PushActionRequest;\n    restart: RestartActionRequest;\n    shutdown: ShutdownActionRequest;\n}\n\nexport const IOSDeviceDetail: React.FunctionComponent<IIOSDeviceDetailProps> = ({\n                                                                                    device,\n                                                                                    tags, deviceTags,\n                                                                                    clearPasscode,\n                                                                                    inventory,\n                                                                                    lock,\n                                                                                    push,\n                                                                                    restart,\n                                                                                    shutdown,\n                                                                                    onAddTag, onChangeTag, onSearchTag,\n                                                                                }: IIOSDeviceDetailProps) => {\n\n    if (!device.device) {\n        return (<div className=\"IOSDeviceDetail\">No device</div>);\n    }\n\n    const attributes = device.device.attributes;\n\n    const niceLastSeen = attributes.last_seen ? distanceInWordsToNow(attributes.last_seen, {addSuffix: true}) : \"Never\";\n\n    return (\n        <div className=\"IOSDeviceDetail\">\n            <Divider hidden/>\n            <Header as=\"h1\">\n                {device.device.attributes.device_name}\n                <Header.Subheader>SN: {device.device.attributes.serial_number}</Header.Subheader>\n            </Header>\n\n            <TagDropdown\n                loading={tags.loading}\n                tags={tags.items}\n                value={deviceTags}\n                onAddItem={onAddTag}\n                onSearch={onSearchTag}\n                onChange={onChangeTag}\n            />\n            <Divider hidden/>\n            <Grid columns={2} className=\"MacOSDeviceDetail\">\n                <Grid.Row>\n                    <Grid.Column>\n                        <List>\n                            <List.Item>\n                                <List.Icon name=\"heartbeat\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>Last Seen</List.Header>\n                                    <List.Description>{niceLastSeen}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"disk outline\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>iOS</List.Header>\n                                    <List.Description>{attributes.os_version} ({attributes.build_version})</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"tag\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>UDID</List.Header>\n                                    <List.Description>{attributes.udid}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"desktop\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>Model</List.Header>\n                                    <List.Description>{attributes.model}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                        </List>\n\n                    </Grid.Column>\n                    <Grid.Column>\n                        <List>\n                            <List.Item>\n                                <List.Icon name=\"bluetooth alternative\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>Bluetooth MAC</List.Header>\n                                    <List.Description>{attributes.bluetooth_mac || \"Not Available\"}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"wifi\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>Wifi MAC</List.Header>\n                                    <List.Description>{attributes.wifi_mac || \"Not Available\"}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"eye\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>Supervised</List.Header>\n                                    <List.Description>{attributes.is_supervised ? \"Yes\" : \"No\"}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"mobile\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>IMEI</List.Header>\n                                    <List.Description>{attributes.imei}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                        </List>\n                    </Grid.Column>\n                </Grid.Row>\n            </Grid>\n            <Grid>\n                <Grid.Row>\n                    <Grid.Column>\n                        <Divider/>\n                        <Button icon labelPosition=\"left\"\n                                disabled={!device.device.attributes.is_supervised}\n                                onClick={() => restart(device.device.id)}>\n                            <Icon name=\"refresh\"/>\n                            Restart\n                        </Button>\n                        <Button icon labelPosition=\"left\"\n                                disabled={!device.device.attributes.is_supervised}\n                                onClick={() => shutdown(device.device.id)}>\n                            <Icon name=\"arrow down\"/>\n                            Shut down\n                        </Button>\n                        <Button icon labelPosition=\"left\" onClick={() => clearPasscode(device.device.id)}>\n                            <Icon name=\"delete\"/>\n                            Clear Passcode\n                        </Button>\n                        <Button icon labelPosition=\"left\" onClick={() => lock(device.device.id)}>\n                            <Icon name=\"lock\"/>\n                            Lock\n                        </Button>\n                        <Button icon labelPosition=\"left\" onClick={() => inventory(device.device.id)}>\n                            <Icon name=\"search\"/>\n                            Full Inventory\n                        </Button>\n                        <Button icon labelPosition=\"left\" onClick={() => push(device.device.id)}>\n                            <Icon name=\"pushed\"/>\n                            Blank Push\n                        </Button>\n                        <ButtonLink to={`/devices/${device.device.id}/rename`}>\n                            Rename\n                        </ButtonLink>\n                        <Menu pointing secondary color=\"purple\" inverted>\n                            <MenuItemLink to={`/devices/${device.device.id}/detail`}>Detail</MenuItemLink>\n                            <MenuItemLink to={`/devices/${device.device.id}/certificates`}>Certificates</MenuItemLink>\n                            <MenuItemLink to={`/devices/${device.device.id}/commands`}>Commands</MenuItemLink>\n                            <MenuItemLink\n                                to={`/devices/${device.device.id}/installed_applications`}>Applications</MenuItemLink>\n                            <MenuItemLink to={`/devices/${device.device.id}/installed_profiles`}>Profiles</MenuItemLink>\n                            <MenuItemLink\n                                to={`/devices/${device.device.id}/available_os_updates`}>Updates</MenuItemLink>\n                        </Menu>\n                    </Grid.Column>\n                </Grid.Row>\n                <Grid.Row>\n                    <Grid.Column>\n                        <Route path=\"/devices/:id/detail\" component={DeviceDetail}/>\n                        <Route path=\"/devices/:id/certificates\" component={DeviceCertificates}/>\n                        <Route path=\"/devices/:id/commands\" component={DeviceCommands}/>\n                        <Route path=\"/devices/:id/installed_applications\" component={DeviceApplications}/>\n                        <Route path=\"/devices/:id/installed_profiles\" component={DeviceProfiles}/>\n                        <Route path=\"/devices/:id/available_os_updates\" component={DeviceOSUpdates}/>\n\n                        <Route path=\"/devices/:id/rename\" component={DeviceRename}/>\n                    </Grid.Column>\n                </Grid.Row>\n            </Grid>\n        </div>\n    );\n};\n"
  },
  {
    "path": "ui/src/components/devices/MacOSDeviceDetail.scss",
    "content": ".MacOSDeviceDetail {\n    i.icon {\n      width: 1.5em;\n    }\n}\n\n.ui.item {\n  padding: 1em 0;\n}"
  },
  {
    "path": "ui/src/components/devices/MacOSDeviceDetail.tsx",
    "content": "import {distanceInWordsToNow} from \"date-fns\";\nimport * as React from \"react\";\nimport {FunctionComponent, SyntheticEvent} from \"react\";\nimport {DeviceState} from \"../../store/device/reducer\";\nimport {ModelIcon} from \"./ModelIcon\";\n\nimport {Route} from \"react-router\";\nimport {DeviceRename} from \"../../containers/DeviceRename\";\nimport {DeviceApplications} from \"../../containers/devices/DeviceApplications\";\nimport {DeviceCertificates} from \"../../containers/devices/DeviceCertificates\";\nimport {DeviceCommands} from \"../../containers/devices/DeviceCommands\";\nimport {DeviceDetail} from \"../../containers/devices/DeviceDetail\";\nimport {DeviceOSUpdates} from \"../../containers/devices/DeviceOSUpdates\";\nimport {DeviceProfiles} from \"../../containers/devices/DeviceProfiles\";\nimport {\n    ClearPasscodeActionRequest, InventoryActionRequest, LockActionRequest,\n    PushActionRequest,\n    RestartActionRequest,\n    ShutdownActionRequest,\n    TestActionRequest,\n} from \"../../store/device/actions\";\nimport {ButtonLink} from \"../semantic-ui/ButtonLink\";\nimport {MenuItemLink} from \"../semantic-ui/MenuItemLink\";\nimport {TagDropdown} from \"../TagDropdown\";\n\nimport {\n    Divider,\n    Icon,\n    Grid,\n    Menu,\n    Button,\n    Header,\n    List,\n    DropdownProps\n} from \"semantic-ui-react\";\n\nimport {ITagsState} from \"../../store/tags/reducer\";\nimport \"./MacOSDeviceDetail.scss\";\n\ninterface IMacOSDeviceDetailProps {\n    device: DeviceState;\n    tags: ITagsState;\n    deviceTags: number[];\n\n    onAddTag: (event: SyntheticEvent<any>, data: object) => void;\n    onChangeTag: (event: SyntheticEvent<HTMLElement>, data: DropdownProps) => void;\n    onSearchTag: (value: string) => void;\n\n    clearPasscode: ClearPasscodeActionRequest;\n    inventory: InventoryActionRequest;\n    lock: LockActionRequest;\n    push: PushActionRequest;\n    restart: RestartActionRequest;\n    shutdown: ShutdownActionRequest;\n}\n\nexport const MacOSDeviceDetail: FunctionComponent<IMacOSDeviceDetailProps> = ({\n         device,\n         tags, deviceTags,\n         clearPasscode,\n         inventory,\n         lock,\n         push,\n         restart,\n         shutdown,\n\n         onAddTag,\n         onChangeTag,\n         onSearchTag,\n        }: IMacOSDeviceDetailProps) => {\n\n        if (!device.device) {\n            return (<div className=\"MacOSDeviceDetail\">No device</div>);\n        }\n\n        const attributes = device.device.attributes;\n\n        const niceLastSeen = attributes.last_seen ? distanceInWordsToNow(attributes.last_seen, { addSuffix: true }) : \"Never\";\n\n        return (\n            <div className=\"MacOSDeviceDetail\">\n                <Divider hidden />\n                <Header as=\"h1\">\n                    {device.device.attributes.device_name}\n                    <Header.Subheader>SN: {device.device.attributes.serial_number}</Header.Subheader>\n                </Header>\n\n                <TagDropdown\n                    loading={tags.loading}\n                    tags={tags.items}\n                    value={deviceTags}\n                    onAddItem={onAddTag}\n                    onSearch={onSearchTag}\n                    onChange={onChangeTag}\n                />\n                <Divider hidden />\n            <Grid columns={2} className=\"MacOSDeviceDetail\">\n                <Grid.Row>\n                    <Grid.Column>\n                        <List>\n                            <List.Item>\n                                <List.Icon name=\"heartbeat\" size=\"large\" verticalAlign=\"middle\" />\n                                <List.Content>\n                                    <List.Header>Last Seen</List.Header>\n                                    <List.Description>{niceLastSeen}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"disk outline\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>macOS</List.Header>\n                                    <List.Description>{attributes.os_version} ({attributes.build_version})</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"tag\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>UDID</List.Header>\n                                    <List.Description>{attributes.udid}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"desktop\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>Model</List.Header>\n                                    <List.Description>{attributes.model}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                        </List>\n\n                    </Grid.Column>\n                    <Grid.Column>\n                        <List>\n                            <List.Item>\n                                <List.Icon name=\"bluetooth alternative\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>Bluetooth MAC</List.Header>\n                                    <List.Description>{attributes.bluetooth_mac || \"Not Available\"}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"wifi\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>Wifi MAC</List.Header>\n                                    <List.Description>{attributes.wifi_mac || \"Not Available\"}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                            <List.Item>\n                                <List.Icon name=\"protect\" size=\"large\" verticalAlign=\"middle\"/>\n                                <List.Content>\n                                    <List.Header>SIP</List.Header>\n                                    <List.Description>{attributes.sip_enabled ? \"Enabled\" : \"Disabled\"}</List.Description>\n                                </List.Content>\n                            </List.Item>\n                        </List>\n                    </Grid.Column>\n                </Grid.Row>\n            </Grid>\n                <Grid>\n                    <Grid.Row>\n                        <Grid.Column>\n                            <Divider />\n                              <Button icon labelPosition=\"left\" onClick={() => restart(device.device.id)}>\n                                <Icon name=\"refresh\"/>\n                                Restart\n                              </Button>\n                              <Button icon labelPosition=\"left\" onClick={() => shutdown(device.device.id)}>\n                                <Icon name=\"arrow down\"/>\n                                Shut down\n                              </Button>\n                              <Button icon labelPosition=\"left\" onClick={() => clearPasscode(device.device.id)}>\n                                <Icon name=\"delete\"/>\n                                Clear Passcode\n                              </Button>\n                              <Button icon labelPosition=\"left\" onClick={() => lock(device.device.id)}>\n                                <Icon name=\"lock\"/>\n                                Lock\n                              </Button>\n                              <Button icon labelPosition=\"left\" onClick={() => inventory(device.device.id)}>\n                                <Icon name=\"search\"/>\n                                Full Inventory\n                              </Button>\n                              <Button icon labelPosition=\"left\" onClick={() => push(device.device.id)}>\n                                <Icon name=\"pushed\"/>\n                                Blank Push\n                              </Button>\n                              <ButtonLink to={`/devices/${device.device.id}/rename`}>\n                                Rename\n                              </ButtonLink>\n                            <Menu pointing secondary color=\"purple\" inverted>\n                                <MenuItemLink to={`/devices/${device.device.id}/detail`}>Detail</MenuItemLink>\n                                <MenuItemLink to={`/devices/${device.device.id}/certificates`}>Certificates</MenuItemLink>\n                                <MenuItemLink to={`/devices/${device.device.id}/commands`}>Commands</MenuItemLink>\n                                <MenuItemLink to={`/devices/${device.device.id}/installed_applications`}>Applications</MenuItemLink>\n                                <MenuItemLink to={`/devices/${device.device.id}/installed_profiles`}>Profiles</MenuItemLink>\n                                <MenuItemLink to={`/devices/${device.device.id}/available_os_updates`}>Updates</MenuItemLink>\n                            </Menu>\n                        </Grid.Column>\n                    </Grid.Row>\n                    <Grid.Row>\n                        <Grid.Column>\n                            <Route path=\"/devices/:id/detail\" component={DeviceDetail}/>\n                            <Route path=\"/devices/:id/certificates\" component={DeviceCertificates}/>\n                            <Route path=\"/devices/:id/commands\" component={DeviceCommands}/>\n                            <Route path=\"/devices/:id/installed_applications\" component={DeviceApplications}/>\n                            <Route path=\"/devices/:id/installed_profiles\" component={DeviceProfiles}/>\n                            <Route path=\"/devices/:id/available_os_updates\" component={DeviceOSUpdates}/>\n\n                            <Route path=\"/devices/:id/rename\" component={DeviceRename}/>\n                        </Grid.Column>\n                    </Grid.Row>\n                </Grid>\n            </div>\n        );\n    };\n"
  },
  {
    "path": "ui/src/components/devices/ModelIcon.tsx",
    "content": "import * as React from \"react\";\nimport {SemanticICONS} from \"semantic-ui-react\";\nimport {Icon} from \"semantic-ui-react\";\n\ninterface IModelIconProps {\n    value: string;\n    title: string;\n}\n\nexport const ModelIcon = (props: IModelIconProps): JSX.Element => {\n    const icons: { [propName: string]: SemanticICONS; } = {\n       \"Mac Pro\": \"computer\",\n       \"MacBook Air\": \"laptop\",\n       \"MacBook Pro\": \"laptop\",\n       \"iMac\": \"desktop\",\n       \"iPad\": \"tablet\",\n       \"iPhone\": \"mobile\",\n    };\n\n    let className: SemanticICONS = \"apple\";\n    if (icons.hasOwnProperty(props.value)) {\n        className = icons[props.value];\n    }\n\n    return <Icon name={className} size=\"large\" title={props.title || props.value} />;\n};\n"
  },
  {
    "path": "ui/src/components/errors/ApiError.tsx",
    "content": "import * as React from \"react\";\nimport {ApiError} from \"redux-api-middleware\";\nimport {Message} from \"semantic-ui-react\";\n\nexport interface IApiErrorProps {\n    error: ApiError;\n}\n\nexport const ApiError: React.FC = ({ error }: IApiErrorProps) => (\n    <Message negative>\n        <Message.Header>Unhandled API Error. This might be a bug</Message.Header>\n        <p>{ error.response.code }</p>\n    </Message>\n);\n"
  },
  {
    "path": "ui/src/components/formik/FormikCheckbox.tsx",
    "content": "import {Field, FieldConfig, FieldProps} from \"formik\";\nimport * as React from \"react\";\nimport {Form, FormProps, Checkbox, CheckboxProps} from \"semantic-ui-react\";\n\nexport type IFormikCheckbox = FieldConfig & CheckboxProps;\n\nexport const FormikCheckbox: React.SFC<IFormikCheckbox> = ({\n    id, name, label, toggle,\n}) => (\n    <Field\n        name={name}\n        render={({field, form}: FieldProps) => {\n            const error = form.touched[name] && form.errors[name];\n            return (\n                <Form.Field name={name}>\n                    <Checkbox toggle={toggle}\n                              id={id || `field_checkbox_${field.name}`}\n                              label={label}\n                              name={field.name}\n                              checked={field.value}\n                              onChange={field.onChange}\n                              />\n                    {error ? (\n                        <span className=\"sui-error-message\">{form.errors[name]}</span>\n                    ) : null}\n                </Form.Field>\n            );\n        }}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/forms/DEPAccountForm.tsx",
    "content": "import * as React from \"react\";\nimport {Form} from \"semantic-ui-react\";\n\nexport class DEPAccountForm extends React.Component {\n    render() {\n        return (\n            <Form>\n\n            </Form>\n        )\n    }\n}"
  },
  {
    "path": "ui/src/components/forms/DEPProfileForm.tsx",
    "content": "import {Field, Form as FormikForm, Formik, FormikBag, FormikErrors, FormikProps, withFormik} from \"formik\";\nimport * as React from \"react\";\nimport {\n    AccordionTitleProps,\n    CheckboxProps,\n    Form,\n    Button,\n    Divider,\n    Icon,\n    Label,\n    Accordion,\n    FormProps\n} from \"semantic-ui-react\";\n\nimport * as Yup from \"yup\";\nimport {DEPProfile, SkipSetupSteps} from \"../../store/dep/types\";\nimport {FormikCheckbox} from \"../formik/FormikCheckbox\";\n\n// The major difference between the form values and the server-side model is that the skip values are boolean inverted\n// so that we can use the language \"show\" instead of hidden / unhide.\nexport interface IDEPProfileFormValues {\n    // show: not present on DEPProfile\n    show: { [SkipSetupSteps: string]: boolean };\n    readonly id?: string;\n    readonly uuid?: string;\n    dep_account_id?: number;\n\n    profile_name: string;\n    url?: string;\n    allow_pairing: boolean;\n    is_supervised: boolean;\n    is_multi_user: boolean;\n    is_mandatory: boolean;\n    await_device_configured: boolean;\n    is_mdm_removable: boolean;\n    support_phone_number: string;\n    auto_advance_setup: boolean;\n    support_email_address?: string;\n    org_magic?: string;\n    // inverted by the form\n    // skip_setup_items: SkipSetupSteps[];\n    department?: string;\n}\n\nexport interface IDEPProfileFormProps {\n    data?: IDEPProfileFormValues;\n    id?: string | number;\n    loading: boolean;\n    activeIndex: number;\n    onSubmit: (values: IDEPProfileFormValues) => void;\n    onClickAccordionTitle: (event: React.MouseEvent<any>, data: AccordionTitleProps) => void;\n}\n\nexport interface IDEPProfileFormState {\n    activeIndex: number;\n}\n\nexport enum DEPProfilePairWithOptions {\n    AnyComputer = \"AnyComputer\",\n    Certificates = \"Certificates\",\n}\n\nconst initialValues: IDEPProfileFormValues = {\n    allow_pairing: true,\n    auto_advance_setup: false,\n    await_device_configured: false,\n    department: \"\",\n    is_mandatory: false,\n    is_mdm_removable: true,\n    is_multi_user: false,\n    is_supervised: true,\n    org_magic: \"\",\n    profile_name: \"\",\n    show: {\n        [SkipSetupSteps.AppleID]: true,\n        [SkipSetupSteps.Biometric]: true,\n        [SkipSetupSteps.Diagnostics]: true,\n        [SkipSetupSteps.DisplayTone]: true,\n        [SkipSetupSteps.Location]: true,\n        [SkipSetupSteps.Passcode]: true,\n        [SkipSetupSteps.Payment]: true,\n        [SkipSetupSteps.Privacy]: true,\n        [SkipSetupSteps.Restore]: true,\n        [SkipSetupSteps.SIMSetup]: true,\n        [SkipSetupSteps.Siri]: true,\n        [SkipSetupSteps.TOS]: true,\n        [SkipSetupSteps.Zoom]: true,\n        [SkipSetupSteps.Android]: true,\n        [SkipSetupSteps.HomeButtonSensitivity]: true,\n        [SkipSetupSteps.iMessageAndFaceTime]: true,\n        [SkipSetupSteps.OnBoarding]: true,\n        [SkipSetupSteps.ScreenTime]: true,\n        [SkipSetupSteps.SoftwareUpdate]: true,\n        [SkipSetupSteps.WatchMigration]: true,\n        [SkipSetupSteps.Appearance]: true,\n        [SkipSetupSteps.FileVault]: true,\n        [SkipSetupSteps.iCloudDiagnostics]: true,\n        [SkipSetupSteps.iCloudStorage]: true,\n        [SkipSetupSteps.Registration]: true,\n        [SkipSetupSteps.ScreenSaver]: true,\n        [SkipSetupSteps.TapToSetup]: true,\n        [SkipSetupSteps.TVHomeScreenSync]: true,\n        [SkipSetupSteps.TVProviderSignIn]: true,\n        [SkipSetupSteps.TVRoom]: true,\n    },\n    support_email_address: \"\",\n    support_phone_number: \"\",\n};\n\nexport interface IInnerFormProps {\n    activeIndex: number | string;\n    id?: number | string;\n    onClickAccordionTitle: (event: React.MouseEvent<any>, data: AccordionTitleProps) => void;\n}\n\nconst InnerForm = (props: IInnerFormProps & FormikProps<IDEPProfileFormValues>) => {\n    const { touched, errors, isSubmitting, activeIndex, handleChange,\n        handleBlur, values, id, handleSubmit, onClickAccordionTitle } = props;\n\n    return (\n        <Form onSubmit={handleSubmit}>\n            <Accordion fluid styled>\n                <Accordion.Title active={activeIndex === 0} index={0} onClick={onClickAccordionTitle}>\n                    <Icon name=\"dropdown\"/>\n                    General\n                </Accordion.Title>\n                <Accordion.Content active={activeIndex === 0}>\n                    <Form.Field required>\n                        <label>Profile Name</label>\n                        <input type=\"text\" name=\"profile_name\"\n                               onChange={handleChange} onBlur={handleBlur}\n                               value={values.profile_name}/>\n\n                        {errors.profile_name &&\n                        touched.profile_name &&\n                        <Label pointing>{errors.profile_name}</Label>}\n                    </Form.Field>\n\n                    <Form.Field>\n                        <label>Support Phone Number</label>\n                        <input type=\"tel\" name=\"support_phone_number\"\n                               onChange={handleChange} onBlur={handleBlur}\n                               value={values.support_phone_number}/>\n                        {errors.support_phone_number &&\n                        touched.support_phone_number &&\n                        errors.support_phone_number}\n                    </Form.Field>\n                    <Form.Field>\n                        <label>Support E-mail Address</label>\n                        <input type=\"email\" name=\"support_email_address\"\n                               onChange={handleChange} onBlur={handleBlur}\n                               value={values.support_email_address}/>\n                        {errors.support_email_address &&\n                        touched.support_email_address &&\n                        errors.support_email_address}\n                    </Form.Field>\n\n                    <Form.Field>\n                        <label>Department</label>\n                        <input type=\"text\" name=\"department\"\n                               onChange={handleChange} onBlur={handleBlur}\n                               value={values.department}/>\n                        {errors.department &&\n                        touched.department &&\n                        errors.department}\n                    </Form.Field>\n\n                    <FormikCheckbox toggle name=\"allow_pairing\" label=\"Allow Pairing\"/>\n                    <FormikCheckbox toggle name=\"is_supervised\"\n                                    label=\"Supervised (will be required in a future version of iOS)\"\n                                    defaultChecked/>\n                    <FormikCheckbox toggle name=\"is_multi_user\" label=\"Shared iPad\" />\n                    <FormikCheckbox toggle name=\"is_mandatory\"\n                                    label=\"Mandatory. User cannot skip Remote Management\" />\n                    <FormikCheckbox toggle name=\"await_device_configured\" label=\"Await Configured\" />\n                    <FormikCheckbox toggle name=\"is_mdm_removable\" label=\"MDM Payload Removable\" />\n                    <FormikCheckbox toggle name=\"auto_advance_setup\" label=\"Auto Advance (tvOS)\" />\n                </Accordion.Content>\n\n                <Accordion.Title active={activeIndex === 1} index={1} onClick={onClickAccordionTitle}>\n                    <Icon name=\"dropdown\"/>\n                    Setup Assistant Steps (Common)\n                </Accordion.Title>\n                <Accordion.Content active={activeIndex === 1}>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.AppleID}`}\n                                    label=\"Show Apple ID Setup\"\n                                    value={SkipSetupSteps.AppleID}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Biometric}`} label=\"Show Touch ID\"\n                                    value={SkipSetupSteps.Biometric}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Diagnostics}`}\n                                    label=\"Show Diagnostics\"\n                                    value={SkipSetupSteps.Diagnostics}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.DisplayTone}`}\n                                    label=\"Show DisplayTone\"\n                                    value={SkipSetupSteps.DisplayTone}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Location}`}\n                                    label=\"Show Location Services\"\n                                    value={SkipSetupSteps.Location}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Passcode}`}\n                                    label=\"Show Passcode Setup\"\n                                    value={SkipSetupSteps.Passcode}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Payment}`} label=\"Show Apple Pay\"\n                                    value={SkipSetupSteps.Payment}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Privacy}`} label=\"Show Privacy\"\n                                    value={SkipSetupSteps.Privacy}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Restore}`}\n                                    label=\"Show Restore from Backup\"\n                                    value={SkipSetupSteps.Restore}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.SIMSetup}`}\n                                    label=\"Show Add Cellular Plan\"\n                                    value={SkipSetupSteps.SIMSetup}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Siri}`} label=\"Show Siri\"\n                                    value={SkipSetupSteps.Siri}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.TOS}`}\n                                    label=\"Show Terms and Conditions\"\n                                    value={SkipSetupSteps.TOS}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Zoom}`} label=\"Show Zoom\"\n                                    value={SkipSetupSteps.Zoom}/>\n                </Accordion.Content>\n                <Accordion.Title active={activeIndex === 2} index={2} onClick={onClickAccordionTitle}>\n                    <Icon name=\"dropdown\" />\n                    Setup Assistant Steps (iOS)\n                </Accordion.Title>\n                <Accordion.Content active={activeIndex === 2}>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Android}`}\n                                    label=\"Show Restore from Android\"\n                                    value={SkipSetupSteps.Android} />\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.HomeButtonSensitivity}`}\n                                    label=\"Show Home Button Sensitivity\"\n                                    value={SkipSetupSteps.HomeButtonSensitivity} />\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.iMessageAndFaceTime}`}\n                                    label=\"Show iMessage and FaceTime\"\n                                    value={SkipSetupSteps.iMessageAndFaceTime} />\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.OnBoarding}`}\n                                    label=\"Show On-Boarding\"\n                                    value={SkipSetupSteps.OnBoarding}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.ScreenTime}`}\n                                    label=\"Show Screen Time\"\n                                    value={SkipSetupSteps.ScreenTime}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.SoftwareUpdate}`}\n                                    label=\"Show Mandatory Software Update Screen\"\n                                    value={SkipSetupSteps.SoftwareUpdate}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.WatchMigration}`}\n                                    label=\"Show Watch Migration\"\n                                    value={SkipSetupSteps.WatchMigration} />\n                </Accordion.Content>\n                <Accordion.Title active={activeIndex === 3} index={3} onClick={onClickAccordionTitle}>\n                    <Icon name=\"dropdown\" />\n                    Setup Assistant Steps (macOS)\n                </Accordion.Title>\n                <Accordion.Content active={activeIndex === 3}>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Appearance}`}\n                                    label=\"Show Choose your Look\"\n                                    value={SkipSetupSteps.Appearance} />\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.FileVault}`}\n                                    label=\"Show FileVault on macOS\"\n                                    value={SkipSetupSteps.FileVault} />\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.iCloudDiagnostics}`}\n                                    label=\"Show iCloud Analytics\"\n                                    value={SkipSetupSteps.iCloudDiagnostics} />\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.iCloudStorage}`}\n                                    label=\"Show iCloud Desktop and Documents\"\n                                    value={SkipSetupSteps.iCloudStorage} />\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.Registration}`}\n                                    label=\"Show Registration\"\n                                    value={SkipSetupSteps.Registration} />\n                </Accordion.Content>\n                <Accordion.Title active={activeIndex === 4} index={4} onClick={onClickAccordionTitle}>\n                    <Icon name=\"dropdown\" />\n                    Setup Assistant Steps (tvOS)\n                </Accordion.Title>\n                <Accordion.Content active={activeIndex === 4}>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.ScreenSaver}`}\n                                    label=\"Show Screen about using Aerial Screensavers in ATV\"\n                                    value={SkipSetupSteps.ScreenSaver}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.TapToSetup}`}\n                                    label=\"Show Tap to Set Up option in ATV\"\n                                    value={SkipSetupSteps.TapToSetup}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.TVHomeScreenSync}`}\n                                    label=\"Show home screen layout sync\"\n                                    value={SkipSetupSteps.TVHomeScreenSync}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.TVProviderSignIn}`}\n                                    label=\"Show TV provider sign in\"\n                                    value={SkipSetupSteps.TVProviderSignIn}/>\n                    <FormikCheckbox toggle name={`show.${SkipSetupSteps.TVRoom}`}\n                                    label='Show \"Where is this Apple TV?\" screen'\n                                    value={SkipSetupSteps.TVRoom}/>\n                </Accordion.Content>\n            </Accordion>\n            <Divider hidden/>\n            <Button type=\"submit\" disabled={isSubmitting} primary>\n                {id ? \"Update\" : \"Create\"}\n            </Button>\n        </Form>\n    );\n};\n\nexport const DEPProfileForm = withFormik<IDEPProfileFormProps, IDEPProfileFormValues>({\n    mapPropsToValues: (props) => {\n        return props.data || initialValues;\n    },\n\n    validationSchema: Yup.object().shape({\n        profile_name: Yup.string().required(\"Required\"),\n    }),\n\n    handleSubmit: (values, formikBag: FormikBag<IDEPProfileFormProps, IDEPProfileFormValues>) => {\n        formikBag.props.onSubmit(values);\n        formikBag.setSubmitting(false);\n    },\n    enableReinitialize: true,\n    displayName: 'DEPProfileForm',\n})(InnerForm);\n"
  },
  {
    "path": "ui/src/components/forms/DeviceAuthForm.tsx",
    "content": "import {Field, Form as FormikForm, Formik, FormikBag, FormikErrors, FormikProps, withFormik} from \"formik\";\nimport * as React from \"react\";\nimport * as Yup from \"yup\";\nimport {Organization} from \"../../store/organization/types\";\n\nimport {\n    Button,\n    Divider,\n    Label,\n    Radio,\n    Form,\n    Grid,\n    Message,\n    Header,\n    Icon,\n    Segment,\n    Checkbox,\n    Item\n} from \"semantic-ui-react\";\n\nimport {SCEPConfiguration} from \"../../store/configuration/types\";\n\nexport interface IDeviceAuthFormValues extends SCEPConfiguration {\n    authentication_method: string;\n}\n\nexport interface IDeviceAuthFormProps {\n    data?: SCEPConfiguration;\n    loading: boolean;\n    activeIndex: number;\n    onSubmit: (values: IDeviceAuthFormValues) => void;\n}\n\nconst initialValues: IDeviceAuthFormValues = {\n    authentication_method: \"internalscep\",\n    key_size: \"1024\",\n    retries: 3,\n    retry_delay: 10,\n};\n\nconst BaseForm = (props: FormikProps<IDeviceAuthFormValues>) => {\n    const { touched, errors, isSubmitting, handleChange,\n        handleBlur, values, handleSubmit } = props;\n\n    return (\n        <Form onSubmit={handleSubmit}>\n            <Grid columns={3} relaxed>\n                <Grid.Column>\n                    <Item>\n                        <Item.Content>\n                            <Item.Header>\n                                <Form.Field>\n                                    <Radio\n                                        id=\"authentication-method-internalscep\"\n                                        label=\"Internal SCEP\"\n                                        name=\"authentication_method\"\n                                        value=\"internalscep\"\n                                        checked={values.authentication_method === \"internalscep\"}\n                                        onChange={handleChange} onBlur={handleBlur}\n                                    />\n                                    {errors.authentication_method &&\n                                    touched.authentication_method &&\n                                    <Label pointing>{errors.authentication_method}</Label>}\n                                </Form.Field>\n                            </Item.Header>\n                        </Item.Content>\n                    </Item>\n                </Grid.Column>\n                <Grid.Column>\n                    <Item>\n                        <Item.Content>\n                            <Item.Header>\n                                <Form.Field>\n                                    <Radio\n                                        id=\"authentication-method-internalca\"\n                                        label=\"Internal CA\"\n                                        name=\"authentication_method\"\n                                        value=\"internalca\"\n                                        checked={values.authentication_method === \"internalca\"}\n                                        onChange={handleChange} onBlur={handleBlur}\n                                    />\n                                    {errors.authentication_method &&\n                                    touched.authentication_method &&\n                                    <Label pointing>{errors.authentication_method}</Label>}\n                                </Form.Field>\n                            </Item.Header>\n                            <Item.Description>Issue PKCS#12 certificates directly from the MDM.</Item.Description>\n                        </Item.Content>\n                    </Item>\n                </Grid.Column>\n                <Grid.Column>\n                    <Item>\n                        <Item.Content>\n                            <Item.Header>\n                                <Form.Field>\n                                    <Radio\n                                        id=\"authentication-method-externalscep\"\n                                        label=\"External SCEP\"\n                                        name=\"authentication_method\"\n                                        value=\"externalscep\"\n                                        checked={values.authentication_method === \"externalscep\"}\n                                        onChange={handleChange} onBlur={handleBlur}\n                                    />\n                                </Form.Field>\n                            </Item.Header>\n                            <Item.Description>\n                                Use an external SCEP service to issue certificates such as Microsoft NDES.\n                            </Item.Description>\n                            <Segment disabled={values.authentication_method !== \"externalscep\"}>\n                                <Form.Field>\n                                    <label>URL</label>\n                                    <input id=\"url\" name=\"url\" type=\"url\"\n                                           value={values.url}\n                                           placeholder=\"http://scep.example.com/scep\"\n                                           onChange={handleChange} onBlur={handleBlur}\n                                           required />\n                                    {errors.url &&\n                                    touched.url &&\n                                    <Label pointing>{errors.url}</Label>}\n                                </Form.Field>\n                                {/*<Form.Field>*/}\n                                    {/*<small className=\"float-right\">*/}\n                                        {/*Optional. Any string that is understood by the SCEP server.*/}\n                                    {/*</small>*/}\n                                    {/*<label>Name</label>*/}\n                                    {/*<input id=\"name\" name=\"name\" type=\"text\"*/}\n                                           {/*onChange={handleChange} onBlur={handleBlur}*/}\n                                           {/*placeholder=\"CA-NAME or organization.org\"/>*/}\n                                    {/*{errors.name &&*/}\n                                    {/*touched.name &&*/}\n                                    {/*<Label pointing>{errors.name}</Label>}*/}\n                                {/*</Form.Field>*/}\n                                <Form.Field>\n                                    <label>Challenge</label>\n                                    <input id=\"challenge\" name=\"challenge\"\n                                           onChange={handleChange} onBlur={handleBlur}\n                                           type=\"password\" />\n                                    {errors.challenge &&\n                                    touched.challenge &&\n                                    <Label pointing>{errors.challenge}</Label>}\n                                </Form.Field>\n                                <small className=\"float-right\">\n                                    Optional. Used as the pre-shared secret for automatic enrollment\n                                </small>\n                            </Segment>\n                        </Item.Content>\n                    </Item>\n                </Grid.Column>\n            </Grid>\n\n            <h2>Certificate Requests</h2>\n            <Message attached>These details explain what kind of information is included in device\n                certificates.</Message>\n            <Segment attached>\n                <Form.Field>\n                    <label>Subject</label>\n                    <input type=\"text\" id=\"subject\" name=\"subject\"\n                           onChange={handleChange} onBlur={handleBlur} value={values.subject}\n                           placeholder=\"O=Commandment/OU=IT/CN=%HardwareUUID%\" />\n                </Form.Field>\n                <Header size=\"tiny\">Key size (in bits)</Header>\n\n                <Form.Field>\n                    <Radio label=\"1024 bits\"\n                           id=\"key-size-1024\"\n                           name=\"key_size\"\n                           value=\"1024\"\n                           checked={values.key_size === \"1024\"}\n                           onChange={handleChange} onBlur={handleBlur}\n                    />\n                </Form.Field>\n                <Form.Field>\n                    <Radio label=\"2048 bits\"\n                           id=\"key-size-2048\"\n                           name=\"key_size\"\n                           value=\"2048\"\n                           checked={values.key_size === \"2048\"}\n                           onChange={handleChange} onBlur={handleBlur}\n                    />\n                </Form.Field>\n\n                <Header size=\"tiny\">Use SCEP key for</Header>\n\n                <Form.Field>\n                    <Checkbox label=\"Signing\" value=\"1\" onChange={handleChange} onBlur={handleBlur} />\n                </Form.Field>\n                <Form.Field>\n                    <Checkbox label=\"Encryption\" value=\"4\" onChange={handleChange} onBlur={handleBlur} />\n                </Form.Field>\n\n                <Form.Field>\n                    <label>Retries</label>\n                    <input type=\"number\" id=\"retries\" name=\"retries\" value={values.retries}\n                           onChange={handleChange} onBlur={handleBlur} />\n                    <p>The number of times the device should retry if the server sends a PENDING response</p>\n                </Form.Field>\n\n                <Form.Field>\n                    <label>Retry Delay</label>\n                    <input type=\"number\" id=\"retry_delay\" name=\"retry_delay\" value={values.retry_delay}\n                           onChange={handleChange} onBlur={handleBlur} />\n                    <p>The number of seconds to wait between subsequent retries. The first retry is attempted without\n                        this delay</p>\n                </Form.Field>\n            </Segment>\n        </Form>\n    );\n};\n\nexport const DeviceAuthForm = withFormik<IDeviceAuthFormProps, IDeviceAuthFormValues>({\n    handleSubmit: (values, formikBag: FormikBag<IDeviceAuthFormProps, IDeviceAuthFormValues>) => {\n        formikBag.props.onSubmit(values);\n        formikBag.setSubmitting(false);\n    },\n    mapPropsToValues: (props) => {\n        return props.data || initialValues;\n    },\n    validationSchema: Yup.object().shape({\n\n    }),\n    enableReinitialize: true,\n    displayName: \"DeviceAuthForm\",\n})(BaseForm);\n"
  },
  {
    "path": "ui/src/components/forms/OrganizationForm.tsx",
    "content": "import {Field, Form as FormikForm, Formik, FormikBag, FormikErrors, FormikProps, withFormik} from \"formik\";\nimport * as React from \"react\";\nimport * as Yup from \"yup\";\nimport {Organization} from \"../../store/organization/types\";\n\nimport {\n    Button,\n    Divider,\n    Label,\n    Form,\n    Header,\n    Icon,\n    Grid\n} from \"semantic-ui-react\";\n\nexport interface IOrganizationFormProps {\n    data?: Organization;\n    id?: string | number;\n    loading: boolean;\n    onSubmit: (values: IOrganizationFormValues) => void;\n}\n\nexport interface IOrganizationFormValues extends Organization {\n\n}\n\nconst initialValues: IOrganizationFormValues = {\n    name: \"\",\n    payload_prefix: \"org.github.cmdmnt\",\n    x509_c: \"US\",\n    x509_o: \"\",\n    x509_ou: \"\",\n    x509_st: \"\",\n};\n\nconst InnerForm = ({\n   handleSubmit,\n   handleChange,\n   handleBlur,\n   values,\n   errors,\n   touched,\n   isSubmitting,\n}: FormikProps<IOrganizationFormValues>) => (\n    <Form onSubmit={handleSubmit}>\n        <Form.Field required>\n            <label>Name</label>\n            <input type=\"text\" name=\"name\"\n                   placeholder=\"Acme Inc.\"\n                   onChange={handleChange} onBlur={handleBlur}\n                   value={values.name}/>\n\n            {errors.name &&\n            touched.name &&\n            <Label pointing>{errors.name}</Label>}\n        </Form.Field>\n\n        <Form.Field required>\n            <label>Payload Prefix</label>\n            <input type=\"text\" name=\"payload_prefix\"\n                   onChange={handleChange} onBlur={handleBlur}\n                   placeholder=\"com.acme\"\n                   value={values.payload_prefix}/>\n\n            {errors.payload_prefix &&\n            touched.payload_prefix &&\n            <Label pointing>{errors.payload_prefix}</Label>}\n        </Form.Field>\n\n        <Header as=\"h3\"><Icon name=\"certificate\"/> Certificate Details</Header>\n        <Grid columns={2}>\n            <Grid.Column>\n                <Form.Field>\n                    <label>X.509 Organizational Unit</label>\n                    <input type=\"text\" name=\"x509_ou\"\n                           onChange={handleChange} onBlur={handleBlur}\n                           placeholder=\"IT Department\"\n                           value={values.x509_ou}/>\n\n                    {errors.x509_ou &&\n                    touched.x509_ou &&\n                    <Label pointing>{errors.x509_ou}</Label>}\n                </Form.Field>\n                <Form.Field>\n                    <label>X.509 Organization</label>\n                    <input type=\"text\" name=\"x509_o\"\n                           onChange={handleChange} onBlur={handleBlur}\n                           placeholder=\"Acme Inc.\"\n                           value={values.x509_o}/>\n\n                    {errors.x509_o &&\n                    touched.x509_o &&\n                    <Label pointing>{errors.x509_o}</Label>}\n                </Form.Field>\n            </Grid.Column>\n            <Grid.Column>\n                <Form.Field>\n                    <label>X.509 State</label>\n                    <input type=\"text\" name=\"x509_st\"\n                           onChange={handleChange} onBlur={handleBlur}\n                           value={values.x509_st}/>\n\n                    {errors.x509_st &&\n                    touched.x509_st &&\n                    <Label pointing>{errors.x509_st}</Label>}\n                </Form.Field>\n                <Form.Field>\n                    <label>X.509 Country Code</label>\n                    <input type=\"text\" name=\"x509_c\"\n                           onChange={handleChange} onBlur={handleBlur}\n                           placeholder=\"US\"\n                           value={values.x509_c}/>\n\n                    {errors.x509_c &&\n                    touched.x509_c &&\n                    <Label pointing>{errors.x509_c}</Label>}\n                </Form.Field>\n            </Grid.Column>\n        </Grid>\n\n        <Divider hidden/>\n\n        <Button type=\"submit\" disabled={isSubmitting} primary>\n            Update\n        </Button>\n    </Form>\n);\n\nexport const OrganizationForm = withFormik<IOrganizationFormProps, IOrganizationFormValues>({\n    displayName: \"OrganizationForm\",\n    enableReinitialize: true,\n    handleSubmit: (values, formikBag: FormikBag<IOrganizationFormProps, IOrganizationFormValues>) => {\n        formikBag.props.onSubmit(values);\n        formikBag.setSubmitting(false);\n    },\n\n    mapPropsToValues: (props) => {\n        return props.data || initialValues;\n    },\n\n    validationSchema: Yup.object().shape({\n        name: Yup.string().required(\"Required\"),\n        payload_prefix: Yup.string().required(\"Required\"),\n    }),\n})(InnerForm);\n"
  },
  {
    "path": "ui/src/components/itunes/MASResult.tsx",
    "content": "import * as React from \"react\";\nimport { Image, List, Grid, Button } from \"semantic-ui-react\";\nimport {ArtworkIconSize, IiTunesSoftwareSearchResult} from \"../../store/applications/itunes\";\n\n\nexport interface IMASResultProps {\n    icon?: ArtworkIconSize;\n    data: IiTunesSoftwareSearchResult;\n    isAdded: boolean;\n    onClickAdd: (result: IiTunesSoftwareSearchResult) => void;\n}\n\nexport const MASResult: React.FunctionComponent = ({ data, onClickAdd, isAdded = false, icon }: IMASResultProps) => (\n    <List.Item>\n        <Image src={icon ? data[icon] : data.artworkUrl100} rounded />\n        <List.Content>\n            <List.Header>{data.trackName}</List.Header>\n            <List.Description>\n                <List.List>\n                    <List.Item>{data.artistName}</List.Item>\n                    <List.Item>Version {data.version}</List.Item>\n                </List.List>\n            </List.Description>\n        </List.Content>\n        <List.Content>\n            <List.Description>\n                <Button size={\"tiny\"} disabled={isAdded} onClick={() => (onClickAdd(data))}>{isAdded ? \"Added\" : \"Add\"}</Button>\n            </List.Description>\n        </List.Content>\n    </List.Item>\n);\n"
  },
  {
    "path": "ui/src/components/modals/DeviceRenameModal.tsx",
    "content": "import * as React from \"react\";\nimport {Modal, Form, FormProps, Button} from \"semantic-ui-react\";\n\nimport {RouteComponentProps} from \"react-router-dom\";\nimport {Device} from \"../../store/device/types\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\n\nexport interface IDeviceRenameModalProps extends RouteComponentProps<any> {\n    device: JSONAPIDataObject<Device>;\n}\n\nexport const DeviceRenameModal: React.FunctionComponent<IDeviceRenameModalProps> =\n    ({ history, device }: IDeviceRenameModalProps) => (\n    <Modal defaultOpen closeIcon onClose={() => {\n        history.goBack();\n    }}>\n        <Modal.Header>Rename Device</Modal.Header>\n        <Modal.Content>\n            <Modal.Description>\n                <Form>\n                    <Form.Field>\n                        <label>Current Device Name</label>\n                        <input type=\"text\" name=\"old_device_name\" value={device.attributes.device_name} readOnly />\n                    </Form.Field>\n                    <Form.Field>\n                        <label>New Device Name</label>\n                        <input type=\"text\" name=\"new_device_name\" />\n                    </Form.Field>\n                </Form>\n            </Modal.Description>\n        </Modal.Content>\n        <Modal.Actions>\n            <Button type=\"submit\" primary>\n                Rename\n            </Button>\n        </Modal.Actions>\n    </Modal>\n);\n"
  },
  {
    "path": "ui/src/components/modals/ProfileUploadModal.tsx",
    "content": "import * as React from \"react\";\nimport Dropzone, {DropFilesEventHandler} from \"react-dropzone\";\nimport {RouteComponentProps} from \"react-router-dom\";\nimport {Modal} from \"semantic-ui-react\";\nimport {UploadActionRequest} from \"../../store/profiles/actions\";\n\nexport interface IProfileUploadModalProps extends RouteComponentProps<any> {\n    upload: UploadActionRequest;\n}\n\nexport const ProfileUploadModal: React.FunctionComponent<IProfileUploadModalProps> = ({ history, upload }: IProfileUploadModalProps) => (\n    <Modal defaultOpen onClose={() => {\n        history.goBack();\n    }}>\n        <Modal.Header>Upload a Profile</Modal.Header>\n        <Modal.Content>\n            <Modal.Description>\n                <Dropzone onDrop={(accepted: File[], rejected: File[]) => {\n                    if (accepted.length === 0) { return; }\n                    const toUpload = accepted[0];\n                    upload(toUpload);\n                }}>\n                    {({getRootProps, getInputProps, isDragActive}) => {\n                        return (\n                            <div\n                                {...getRootProps()}\n                                >\n                              <input {...getInputProps()} />\n                                {\n                                    isDragActive ?\n                                        <p>Drop files here...</p> :\n                                        <p>Try dropping some files here, or click to select files to upload.</p>\n                                }\n                            </div>\n                        )\n                    }}\n                </Dropzone>\n            </Modal.Description>\n        </Modal.Content>\n    </Modal>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/AppName.tsx",
    "content": "import * as React from \"react\";\nimport {Link} from \"react-router-dom\";\nimport {CellInfo} from \"react-table\";\n\nexport const AppName: React.FunctionComponent<CellInfo> = ({ value, original }: CellInfo) => (\n    <Link to={`/applications/id/${original.id}`}>\n        <span>{value ? value : original.attributes.display_name}</span>\n    </Link>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/ApplicationType.tsx",
    "content": "import * as React from \"react\";\nimport {CellInfo} from \"react-table\";\nimport {Icon} from \"semantic-ui-react\";\nimport {FunctionComponent, ReactElement, ReactNode} from \"react\";\n\nconst icons: { [status: string]: ReactNode } = {\n    \"appstore_mac\": <Icon name=\"laptop\" color=\"grey\" />,\n    \"appstore_ios\": <Icon name=\"mobile\" color=\"grey\" />,\n};\n\nexport const ApplicationType: FunctionComponent<CellInfo> = ({ value }: CellInfo) => (\n    <span>{icons.hasOwnProperty(value) ? icons[value] : null}</span>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/ByteSize.tsx",
    "content": "import * as byteSize from \"byte-size\";\nimport * as React from \"react\";\nimport {CellInfo} from \"react-table\";\n\nexport const ByteSize: React.FunctionComponent<CellInfo> = ({ value, original }) => (\n    <span>{value ? byteSize(value).value + \" \" + byteSize(value).unit : \"\"}</span>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/CommandStatus.tsx",
    "content": "import * as React from \"react\";\nimport {CellInfo} from \"react-table\";\nimport {Icon, IconProps} from \"semantic-ui-react\";\nimport {FunctionComponent, ReactElement} from \"react\";\n\nconst icons: { [status: string]: JSX.Element } = {\n    \"CommandStatus.Acknowledged\": <Icon name=\"check\" color=\"grey\" />,\n    \"CommandStatus.Error\": <Icon name=\"ban\" color=\"red\" />,\n    \"CommandStatus.NotNow\": <Icon name=\"question circle outline\" color=\"blue\" />,\n    \"CommandStatus.Queued\": <Icon name=\"wait\" color=\"blue\" />,\n    \"CommandStatus.Sent\": <Icon name=\"paper plane\" color=\"green\" />,\n};\n\nexport const CommandStatus: FunctionComponent<CellInfo> = ({ value }: CellInfo) => (\n    <span>{icons.hasOwnProperty(value) ? icons[value] : null}</span>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/DEPAccountServerName.tsx",
    "content": "import * as React from \"react\";\nimport {Link} from \"react-router-dom\";\nimport {CellInfo} from \"react-table\";\n\nexport const DEPAccountServerName: React.FunctionComponent<CellInfo> = ({ value, original }) => (\n    <Link to={`/dep/accounts/${original.id}`}>\n        <span>{value ? value : original.attributes.server_name}</span>\n    </Link>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/DEPProfileName.tsx",
    "content": "import * as React from \"react\";\nimport {Link} from \"react-router-dom\";\nimport {CellInfo} from \"react-table\";\n\nexport const DEPProfileName: React.FunctionComponent<CellInfo> = ({ value, original }) => {\n    return (\n        <Link to={`/dep/accounts/${original.relationships.dep_account.data.id}/profiles/${original.id}`}>\n            <span>{value ? value : original.attributes.profile_name}</span>\n        </Link>);\n};\n"
  },
  {
    "path": "ui/src/components/react-table/DeviceName.tsx",
    "content": "import * as React from \"react\";\nimport {Link} from \"react-router-dom\";\nimport {CellInfo} from \"react-table\";\n\nexport const DeviceName: React.FunctionComponent<CellInfo> = ({ value, original }) => (\n    <Link to={`/devices/${original.id}`}>\n        <span>{value ? value : original.attributes.description}</span>\n    </Link>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/ObjectLink.tsx",
    "content": "import * as React from \"react\";\nimport {CellInfo} from \"react-table\";\nimport {Link} from \"react-router-dom\";\n\nexport const ObjectLink: React.FunctionComponent<CellInfo> = ({ value, original }: CellInfo) => (\n    <Link to={`/objtype/id/${original.id}`}>\n        <span>{value ? value : original.attributes.display_name}</span>\n    </Link>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/ProfileName.tsx",
    "content": "import * as React from \"react\";\nimport {Link} from \"react-router-dom\";\nimport {CellInfo} from \"react-table\";\n\nexport const ProfileName: React.FunctionComponent<CellInfo> = ({ value, original }: CellInfo) => (\n    <Link to={`/profiles/id/${original.id}`}>\n        <span>{value ? value : original.attributes.display_name}</span>\n    </Link>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/RelativeToNow.tsx",
    "content": "import {distanceInWordsToNow, parse} from \"date-fns\";\nimport * as React from \"react\";\nimport {CellInfo} from \"react-table\";\n\nexport const RelativeToNow: React.FunctionComponent<CellInfo> = ({ value, original }) => (\n    <span>{value ? distanceInWordsToNow(parse(value), {addSuffix: true}) : \"\"}</span>\n);\n"
  },
  {
    "path": "ui/src/components/react-table/SUISelectionTools.tsx",
    "content": "import * as React from \"react\";\nimport {Input, Dropdown} from \"semantic-ui-react\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\nimport {Tag} from \"../../store/tags/types\";\nimport {ActionMenu} from \"../ActionMenu\";\n\nexport interface ISUISelectionTools {\n    loading: boolean;\n    selectionCount: number;\n    tags: Array<JSONAPIDataObject<Tag>>;\n}\n\nexport const SUISelectionTools: React.FunctionComponent<ISUISelectionTools> = (props: ISUISelectionTools) => (\n    <div>\n        <Dropdown text=\"Tag device(s)\" icon=\"filter\" floating labeled button className=\"icon\" disabled={props.selectionCount < 1}>\n            <Dropdown.Menu>\n                <Input icon=\"search\" iconPosition=\"left\" className=\"search\" />\n                <Dropdown.Divider />\n                <Dropdown.Header icon=\"tags\" content=\"Tag Label\" />\n                <Dropdown.Menu scrolling>\n                    {props.tags && props.tags.map((tag) =>\n                        <Dropdown.Item key={tag.id}\n                                       value={tag.id}\n                                       text={tag.attributes.name}\n                                       label={{ color: tag.attributes.color, empty: true, circular: true }}/>)}\n                </Dropdown.Menu>\n            </Dropdown.Menu>\n        </Dropdown>\n\n        <ActionMenu enabledActions={[\"BLANK_PUSH\"]}/>\n    </div>\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/AppDeployStatusTable.tsx",
    "content": "import {distanceInWordsToNow} from \"date-fns\";\nimport * as React from \"react\";\nimport ReactTable, {CellInfo, TableProps} from \"react-table\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\nimport {ManagedApplication} from \"../../store/applications/types\";\n\nexport interface IAppDeployStatusTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<ManagedApplication>>;\n    onFetchData: (state: any, instance: any) => void;\n}\n\nconst columns = [\n    {\n\n    },\n    {\n        Header: \"Bundle ID\",\n        accessor: \"attributes.bundle_id\",\n        id: \"bundle_id\",\n    },\n    {\n        Header: \"Status\",\n        accessor: \"attributes.status\",\n        id: \"status\",\n    },\n];\n\nexport const AppDeployStatusTable = ({ data, ...props }: IAppDeployStatusTableProps & Partial<TableProps>) => (\n    <ReactTable\n        manual\n        filterable\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/ApplicationsTable.tsx",
    "content": "import * as React from \"react\";\nimport ReactTable, {Column, TableProps} from \"react-table\";\nimport selectTableHoc from \"react-table/lib/hoc/selectTable\";\n\nimport {Application} from \"../../store/applications/types\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\nimport {AppName} from \"../react-table/AppName\";\nimport {ApplicationType} from \"../react-table/ApplicationType\";\n// import \"react-table/react-table.css\";\n\nexport interface IApplicationsTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<Application>>;\n    onToggleSelection: () => void;\n    onToggleAll: () => void;\n}\n\nconst columns: Column[] = [\n    {\n        Cell: ApplicationType,\n        Header: \"Type\",\n        accessor: \"attributes.discriminator\",\n        id: \"discriminator\",\n        style: { textAlign: \"center\" },\n        width: 50,\n    },\n    {\n        Header: \"Developer\",\n        accessor: \"attributes.artist_name\",\n        id: \"artist_name\",\n    },\n    {\n        Cell: AppName,\n        Header: \"Name\",\n        accessor: \"attributes.display_name\",\n        id: \"display_name\",\n    },\n    {\n        Header: \"Version\",\n        accessor: \"attributes.version\",\n        id: \"version\",\n    },\n];\n\nconst ReactSelectTable = selectTableHoc(ReactTable);\n\nexport const ApplicationsTable = ({ data, ...props }: IApplicationsTableProps & Partial<TableProps>) => (\n    <ReactSelectTable\n        keyField=\"id\"\n        selectType=\"checkbox\"\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/DEPAccountsTable.tsx",
    "content": "import {distanceInWordsToNow} from \"date-fns\";\nimport * as React from \"react\";\nimport ReactTable, {CellInfo, TableProps, Column} from \"react-table\";\nimport selectTableHoc from \"react-table/lib/hoc/selectTable\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\nimport {DEPAccount} from \"../../store/dep/types\";\nimport {DEPAccountServerName} from \"../react-table/DEPAccountServerName\";\n// import \"react-table/react-table.css\";\n\nexport interface IDEPAccountsTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<DEPAccount>>;\n    onToggleSelection: () => void;\n    onToggleAll: () => void;\n}\n\nconst columns: Column[] = [\n    {\n        Cell: DEPAccountServerName,\n        Header: \"Server Name\",\n        accessor: \"attributes.server_name\",\n        id: \"server_name\",\n    },\n    {\n        Header: \"Organization\",\n        accessor: \"attributes.org_name\",\n        id: \"org_name\",\n    },\n];\n\nconst ReactSelectTable = selectTableHoc(ReactTable);\n\nexport const DEPAccountsTable = ({ data, ...props }: IDEPAccountsTableProps & Partial<TableProps>) => (\n    <ReactSelectTable\n        manual\n        keyField=\"id\"\n        selectType=\"checkbox\"\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/DEPProfilesTable.tsx",
    "content": "import * as React from \"react\";\nimport ReactTable, {TableProps, Column} from \"react-table\";\nimport selectTableHoc from \"react-table/lib/hoc/selectTable\";\nimport {DEPAccount, DEPProfile} from \"../../store/dep/types\";\nimport {DEPProfileName} from \"../react-table/DEPProfileName\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\n// import \"react-table/react-table.css\";\n\nexport interface IDEPProfilesTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<DEPProfile>>;\n    onToggleSelection: () => void;\n    onToggleAll: () => void;\n}\n\nconst columns: Column[] = [\n    {\n        Cell: DEPProfileName,\n        Header: \"Name\",\n        accessor: \"attributes.profile_name\",\n        id: \"profile_name\",\n    },\n    {\n        Header: \"UUID\",\n        accessor: \"attributes.uuid\",\n        id: \"uuid\",\n    },\n];\n\nconst ReactSelectTable = selectTableHoc(ReactTable);\n\nexport const DEPProfilesTable = ({ data, ...props }: IDEPProfilesTableProps & Partial<TableProps>) => (\n    <ReactSelectTable\n        keyField=\"id\"\n        selectType=\"checkbox\"\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/DeviceApplicationsTable.tsx",
    "content": "import {distanceInWordsToNow} from \"date-fns\";\nimport * as React from \"react\";\nimport ReactTable, {CellInfo, TableProps} from \"react-table\";\nimport {InstalledApplication} from \"../../store/device/types\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\nimport {ByteSize} from \"../react-table/ByteSize\";\nimport {DEPAccount} from \"../../store/dep/types\";\n\nexport interface IDeviceApplicationsTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<InstalledApplication>>;\n    onFetchData: (state: any, instance: any) => void;\n}\n\nconst columns = [\n    {\n        Header: \"Name\",\n        accessor: \"attributes.name\",\n        id: \"name\",\n    },\n    {\n        Header: \"Version\",\n        accessor: \"attributes.short_version\",\n        id: \"short_version\",\n        maxWidth: 140,\n    },\n    {\n        Cell: ByteSize,\n        Header: \"Size\",\n        accessor: \"attributes.bundle_size\",\n        id: \"bundle_size\",\n        maxWidth: 100,\n    },\n];\n\nexport const DeviceApplicationsTable = ({ data, ...props }: IDeviceApplicationsTableProps & Partial<TableProps>) => (\n    <ReactTable\n        manual\n        filterable\n        keyField=\"id\"\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/DeviceCertificatesTable.tsx",
    "content": "import {distanceInWordsToNow} from \"date-fns\";\nimport * as React from \"react\";\nimport ReactTable, {CellInfo, TableProps} from \"react-table\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\nimport {InstalledCertificate} from \"../../store/device/types\";\nimport {DEPAccount} from \"../../store/dep/types\";\n\nexport interface IDeviceCertificateTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<InstalledCertificate>>;\n    onFetchData: (state: any, instance: any) => void;\n}\n\nconst columns = [\n    {\n        Header: \"Common Name\",\n        accessor: (certificate: JSONAPIDataObject<InstalledCertificate>) => certificate.attributes.x509_cn,\n        id: \"x509_cn\",\n    },\n];\n\nexport const DeviceCertificatesTable = ({ data, ...props }: IDeviceCertificateTableProps & Partial<TableProps>) => (\n    <ReactTable\n        manual\n        filterable\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/DeviceCommandsTable.tsx",
    "content": "import * as React from \"react\";\nimport ReactTable, {TableProps} from \"react-table\";\nimport {Command} from \"../../store/device/types\";\nimport {RelativeToNow} from \"../react-table/RelativeToNow\";\nimport {CommandStatus} from \"../react-table/CommandStatus\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\nimport {DEPAccount} from \"../../store/dep/types\";\n\nexport interface IDeviceCommandsTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<Command>>;\n    onFetchData: (state: any, instance: any) => void;\n}\n\nconst columns = [\n    {\n        Cell: CommandStatus,\n        Header: \"Status\",\n        accessor: \"attributes.status\",\n        id: \"status\",\n        maxWidth: 50,\n        style: { textAlign: \"center\" },\n    },\n    {\n        Header: \"Type\",\n        accessor: \"attributes.request_type\",\n        id: \"request_type\",\n    },\n    {\n        Cell: RelativeToNow,\n        Header: \"Sent\",\n        accessor: \"attributes.sent_at\",\n        id: \"sent_at\",\n    },\n];\n\nexport const DeviceCommandsTable = ({ data, ...props }: IDeviceCommandsTableProps & Partial<TableProps>) => (\n    <ReactTable\n        manual\n        filterable\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/DeviceProfilesTable.tsx",
    "content": "import * as React from \"react\";\nimport ReactTable, {TableProps} from \"react-table\";\nimport {InstalledProfile} from \"../../store/device/types\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\nimport {DEPAccount} from \"../../store/dep/types\";\n\nexport interface IDeviceProfilesTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<InstalledProfile>>;\n    onFetchData: (state: any, instance: any) => void;\n}\n\nconst columns = [\n    {\n        Header: \"Display Name\",\n        accessor: \"attributes.payload_display_name\",\n        id: \"payload_display_name\",\n    },\n    {\n        Header: \"Identifier\",\n        accessor: \"attributes.payload_identifier\",\n        id: \"payload_identifier\",\n    },\n];\n\nexport const DeviceProfilesTable = ({ data, ...props }: IDeviceProfilesTableProps & Partial<TableProps>) => (\n    <ReactTable\n        manual\n        filterable\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/DeviceUpdatesTable.tsx",
    "content": "import * as React from \"react\";\nimport ReactTable, {TableProps} from \"react-table\";\nimport {AvailableOSUpdate} from \"../../store/device/types\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\n\nexport interface IDeviceUpdatesTableProps extends Partial<TableProps> {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<AvailableOSUpdate>>;\n    onFetchData: (state: any, instance: any) => void;\n}\n\nconst columns = [\n    {\n        Header: \"Product ID\",\n        accessor: \"attributes.product_key\",\n        id: \"product_key\",\n        maxWidth: 140,\n    },\n    {\n        Header: \"Name\",\n        accessor: \"attributes.human_readable_name\",\n        id: \"human_readable_name\",\n    },\n    {\n        Header: \"Version\",\n        accessor: \"attributes.version\",\n        id: \"version\",\n        maxWidth: 100,\n    },\n];\n\nexport const DeviceUpdatesTable = ({ data, ...props }: IDeviceUpdatesTableProps) => (\n    <ReactTable\n        manual\n        filterable\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/DevicesTable.tsx",
    "content": "import {distanceInWordsToNow} from \"date-fns\";\nimport * as React from \"react\";\nimport ReactTable, {CellInfo, TableProps} from \"react-table\";\nimport selectTableHoc from \"react-table/lib/hoc/selectTable\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\n// import \"react-table/react-table.css\";\nimport {Device} from \"../../store/device/types\";\nimport {ModelIcon} from \"../devices/ModelIcon\";\nimport {DeviceName} from \"../react-table/DeviceName\";\nimport {DEPAccount} from \"../../store/dep/types\";\n\nexport interface IDevicesTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<Device>>;\n    toggleSelection: (key: string, shiftKeyPressed: boolean, row: any) => any;\n    toggleAll: () => any;\n    isSelected: (key: string) => boolean;\n    onFetchData: (state: any, instance: any) => void;\n}\n\nconst columns = [\n    {\n        Cell: ModelIcon,\n        Header: \"\",\n        accessor: \"attributes.model_name\",\n        filterable: false,\n        id: \"model_name\",\n        maxWidth: 40,\n        style: { textAlign: \"center\" },\n    },\n    {\n        Cell: DeviceName,\n        Header: \"Name\",\n        accessor: (device: JSONAPIDataObject<Device>) => device.attributes.device_name,\n        id: \"device_name\",\n    },\n    {\n        Header: \"Serial\",\n        accessor: \"attributes.serial_number\",\n        id: \"serial_number\",\n    },\n    {\n        Header: \"OS\",\n        accessor: \"attributes.os_version\",\n        id: \"os_version\",\n        maxWidth: 100,\n    },\n    {\n        Cell: (props: CellInfo) => props.value ? distanceInWordsToNow(props.value, {addSuffix: true}) : \"never\",\n        Header: \"Last Seen\",\n        accessor: \"attributes.last_seen\",\n        filterable: false,\n        id: \"last_seen\",\n    },\n];\n\nconst ReactSelectTable = selectTableHoc(ReactTable);\n\nexport const DevicesTable = ({ data, ...props }: IDevicesTableProps & Partial<TableProps>) => (\n    <ReactSelectTable\n        manual\n        filterable\n        keyField=\"id\"\n        selectType=\"checkbox\"\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/react-tables/ProfilesTable.tsx",
    "content": "import {distanceInWordsToNow} from \"date-fns\";\nimport * as React from \"react\";\nimport ReactTable, {CellInfo, TableProps, Column} from \"react-table\";\nimport selectTableHoc from \"react-table/lib/hoc/selectTable\";\nimport {JSONAPIDataObject} from \"../../store/json-api\";\n// import \"react-table/react-table.css\";\nimport {AvailableOSUpdate, Device} from \"../../store/device/types\";\nimport {Profile} from \"../../store/profiles/types\";\nimport {DeviceName} from \"../react-table/DeviceName\";\nimport {ProfileName} from \"../react-table/ProfileName\";\n\nexport interface IProfilesTableProps {\n    loading: boolean;\n    data: Array<JSONAPIDataObject<Profile>>;\n    onToggleSelection: () => void;\n    onToggleAll: () => void;\n}\n\nconst columns: Column[] = [\n    {\n        Cell: ProfileName,\n        Header: \"Name\",\n        accessor: (device: JSONAPIDataObject<Device>) => device.attributes.device_name,\n        id: \"display_name\",\n    },\n    {\n        Header: \"UUID\",\n        accessor: \"attributes.uuid\",\n        id: \"uuid\",\n    },\n];\n\nconst ReactSelectTable = selectTableHoc(ReactTable);\n\nexport const ProfilesTable = ({ data, ...props }: IProfilesTableProps & Partial<TableProps>) => (\n    <ReactSelectTable\n        manual\n        keyField=\"id\"\n        selectType=\"checkbox\"\n        data={data}\n        columns={columns}\n        {...props}\n    />\n);\n"
  },
  {
    "path": "ui/src/components/semantic-ui/ButtonLink.tsx",
    "content": "import * as React from \"react\";\nimport {Link, Route} from \"react-router-dom\";\nimport {Button, ButtonProps } from \"semantic-ui-react\";\n\ninterface IButtonLinkProps extends ButtonProps {\n    to: string;\n}\n\n/**\n * The ButtonLink component mixes the visual style and behaviour of a semantic-ui-react button with a react-router Link.\n * @param {IButtonLinkProps} props\n * @returns {any}\n * @constructor\n */\nexport const ButtonLink = (props: IButtonLinkProps) => (\n    <Button as={Link} to={props.to} {...props} />\n);\n"
  },
  {
    "path": "ui/src/components/semantic-ui/MenuItemLink.tsx",
    "content": "import * as React from \"react\";\nimport {Link, Route} from \"react-router-dom\";\nimport {Menu} from \"semantic-ui-react\";\n\ninterface IMenuItemLinkProps {\n    to: string;\n    activeOnlyWhenExact?: boolean;\n    header?: boolean;\n    children: any;\n}\n\nexport const MenuItemLink = ({ to, children, activeOnlyWhenExact = false, header = false }: IMenuItemLinkProps) => (\n    <Route path={to} exact={activeOnlyWhenExact} children={({ match }) => (\n        <Menu.Item as={Link} to={to} active={match ? true : undefined} header={header}>{children}</Menu.Item>\n    )}/>\n);\n"
  },
  {
    "path": "ui/src/components/vpp/VPPAccountDetail.tsx",
    "content": "import * as React from \"react\";\n\nimport {\n    Button,\n    Header,\n    Icon,\n    Segment,\n} from \"semantic-ui-react\";\n\nimport {format} from \"date-fns\";\nimport {VPPAccount} from \"../../store/configuration/types\";\n\nexport interface IVPPAccountDetailProps extends VPPAccount {\n}\n\nexport const VPPAccountDetail: React.StatelessComponent<IVPPAccountDetailProps> = (props: IVPPAccountDetailProps) => (\n    <Segment>\n        <Header as=\"h1\">\n            <Icon name=\"ticket\" />\n            <Header.Content>\n                VPP Token ({props.org_name})\n            </Header.Content>\n        </Header>\n        Expires {format(props.exp_date)}\n        <Button icon=\"download\" content=\".vpptoken\" />\n\n    </Segment>\n);\n"
  },
  {
    "path": "ui/src/constants.ts",
    "content": "export const CERTIFICATE_PURPOSE: {[propName: string]: string} = {\n    \"mdm.pushcert\": \"APNS MDM Push Certificate\",\n    \"mdm.webcrt\": \"MDM Web Server Certificate\",\n    \"mdm.cacert\": \"MDM SCEP CA Certificate\",\n};\n"
  },
  {
    "path": "ui/src/containers/AppStorePage.tsx",
    "content": "import * as React from \"react\";\n\nimport {\n    Breadcrumb,\n    List,\n    Container,\n    Divider,\n    Header,\n} from \"semantic-ui-react\";\n\n\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {MASResult} from \"../components/itunes/MASResult\";\nimport {SearchInput} from \"../components/SearchInput\";\nimport {RootState} from \"../reducers\";\nimport {\n    itunesSearch,\n    ItunesSearchAction,\n    post,\n    PostActionRequest,\n    postAppStoreIos,\n    postAppStoreMac,\n} from \"../store/applications/actions\";\nimport {\n    ArtworkIconSize,\n    EntityType,\n    IiTunesSearchResult,\n    IiTunesSoftwareSearchResult,\n    MediaType,\n} from \"../store/applications/itunes\";\n\nimport {Link} from \"react-router-dom\";\nimport {Application} from \"../store/applications/types\";\n\ninterface IRouteProps {\n    entity: EntityType;\n}\n\nexport interface IDispatchProps {\n    itunesSearch: ItunesSearchAction;\n    post: PostActionRequest;\n    postAppStoreMac: PostActionRequest;\n    postAppStoreIos: PostActionRequest;\n}\n\nexport interface IStateProps {\n    storeCountry: string;\n    loading: boolean;\n    itunesSearchResult: IiTunesSearchResult;\n    itunesStoreIdsAdded: number[];\n}\n\nexport type AppStorePageProps = IDispatchProps & IStateProps & RouteComponentProps<IRouteProps>;\n\nexport class UnconnectedAppStorePage extends React.Component<AppStorePageProps, any> {\n    public render() {\n        const { itunesSearchResult, itunesStoreIdsAdded, loading, match: { params: { entity }}} = this.props;\n\n        return (\n            <Container>\n                <Divider hidden/>\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/applications`}>Applications</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section>Find a {entity === \"macSoftware\" ? \"macOS\" : \"iOS\"} app</Breadcrumb.Section>\n                </Breadcrumb>\n                <Divider hidden/>\n                <Header as=\"h1\">Find a new {entity === \"macSoftware\" ? \"macOS\" : \"iOS\"} App Store App\n                    <Header.Subheader>Add an App Store app as a managed application.</Header.Subheader>\n                </Header>\n\n                <SearchInput duration={400} loading={loading} onSearch={this.handleSearch}/>\n\n                {itunesSearchResult &&\n                    <div>\n                      <p>Your search returned <strong>{itunesSearchResult.resultCount}</strong> result(s)</p>\n                    <List relaxed=\"very\">\n                        {itunesSearchResult.results.map((result: IiTunesSoftwareSearchResult) => (\n                            <MASResult key={result.trackId}\n                                       data={result}\n                                       icon={ArtworkIconSize.Sixty}\n                                       onClickAdd={this.handleClickAdd}\n                                       isAdded={itunesStoreIdsAdded.indexOf(result.trackId) !== -1}\n                            />\n                        ))}\n                    </List>\n                    </div>\n                }\n            </Container>\n        );\n    }\n\n    private handleClickAdd = (result: IiTunesSoftwareSearchResult) => {\n        const entity = this.props.match.params.entity;\n        const app: Application = {\n            bundle_id: result.bundleId,\n            description: result.description,\n            display_name: result.trackName,\n            itunes_store_id: result.trackId,\n            version: result.version,\n\n            country: this.props.storeCountry,\n\n            artist_id: result.artistId,\n            artist_name: result.artistName,\n            artist_view_url: result.artistViewUrl,\n            artwork_url60: result.artworkUrl60,\n            artwork_url100: result.artworkUrl100,\n            artwork_url512: result.artworkUrl512,\n            release_notes: result.releaseNotes,\n            release_date: result.releaseDate,\n            minimum_os_version: result.minimumOsVersion,\n            file_size_bytes: result.fileSizeBytes,\n        };\n\n        if (entity === EntityType.macSoftware) {\n            this.props.postAppStoreMac(app)\n        } else if (entity === EntityType.software) {\n            this.props.postAppStoreIos(app);\n        } else {\n            // cant really post this\n        }\n\n    };\n\n    private handleSearch = (value: string) => {\n        const { match: { params: { entity }}, storeCountry} = this.props;\n        this.props.itunesSearch(value, storeCountry, MediaType.software, entity);\n    };\n}\n\nexport const AppStorePage = connect(\n    (state: RootState, ownProps?: any) => ({\n        itunesSearchResult: state.applications.itunesSearchResult,\n        itunesStoreIdsAdded: state.applications.itunesStoreIdsAdded,\n        loading: state.applications.itunesSearchResultLoading,\n        storeCountry: state.applications.storeCountry,\n    }),\n    (dispatch: Dispatch, ownProps?: any) => bindActionCreators({\n        itunesSearch,\n        post,\n        postAppStoreIos,\n        postAppStoreMac,\n    }, dispatch),\n)(UnconnectedAppStorePage);\n"
  },
  {
    "path": "ui/src/containers/ApplicationPage.tsx",
    "content": "import * as React from \"react\";\nimport {SyntheticEvent} from \"react\";\nimport {connect} from \"react-redux\";\nimport {Route, RouteComponentProps} from \"react-router\";\nimport {Link} from \"react-router-dom\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {Breadcrumb, Container, Divider, Grid, Header, Image, Menu} from \"semantic-ui-react\";\nimport {DropdownProps} from \"semantic-ui-react\";\nimport {MenuItemLink} from \"../components/semantic-ui/MenuItemLink\";\nimport {TagDropdown} from \"../components/TagDropdown\";\nimport {isArray} from \"../guards\";\nimport {RootState} from \"../reducers\";\nimport {\n    patchRelationship, PatchRelationshipActionRequest,\n    read, ReadActionRequest,\n} from \"../store/applications/actions\";\nimport {IApplicationState} from \"../store/applications/reducer\";\nimport {JSONAPIRelationship, JSONAPIResourceIdentifier} from \"../store/json-api\";\nimport {\n    index as fetchTags,\n    IndexActionRequest,\n    post as createTag,\n    PostActionRequest as PostTagActionRequest,\n} from \"../store/tags/actions\";\nimport {ITagsState} from \"../store/tags/reducer\";\nimport {Tag} from \"../store/tags/types\";\nimport {ApplicationDeviceStatus} from \"./applications/ApplicationDeviceStatus\";\nimport {Relationship, RelationshipData, Relationships} from \"../json-api-v1\";\n\ninterface IRouteProps {\n    id: string;\n}\n\nexport interface IDispatchProps {\n    read: ReadActionRequest;\n    fetchTags: IndexActionRequest;\n    patchRelationship: PatchRelationshipActionRequest;\n}\n\nexport interface IStateProps {\n    application: IApplicationState;\n    tags: ITagsState;\n}\n\nclass UnconnectedApplicationPage extends React.Component<IDispatchProps & IStateProps & RouteComponentProps<IRouteProps>, any> {\n\n    public componentWillMount?() {\n        const { match: { params: { id } } } = this.props;\n\n        this.props.read(id, [\"tags\"]);\n        this.props.fetchTags(40);\n    }\n\n    public render() {\n        const {\n            match: { params: { id } },\n            application: { data, loading },\n            tags,\n        } = this.props;\n\n        let appTags: number[] = [];\n\n        if (data && data.data.relationships && data.data.relationships.tags) {\n            if (isArray(data.data.relationships.tags.data)) {\n                appTags = data.data.relationships.tags.data.map((t: JSONAPIResourceIdentifier) => parseInt(t.id, 0));\n            } else {\n                appTags = [parseInt(data.data.relationships.tags.data.id, 0)];\n            }\n        }\n\n        return (\n            <Container>\n                <Divider hidden />\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/applications`}>Applications</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section>{data ? data.data.attributes.display_name : \"App\"}</Breadcrumb.Section>\n                </Breadcrumb>\n                <Divider hidden />\n\n                <Grid columns={2}>\n                    <Grid.Column width={4}>\n                        <Image size=\"medium\" rounded src={data ? data.data.attributes.artwork_url512 : null}/>\n                    </Grid.Column>\n                    <Grid.Column width={12}>\n                        <Header as=\"h1\">\n                            {data ? data.data.attributes.display_name + \" \" + data.data.attributes.version : \"Loading...\"}\n                            <Header.Subheader>\n                                {data ? data.data.attributes.artist_name : \"Loading...\"}\n                            </Header.Subheader>\n                        </Header>\n\n                        <Header as=\"h4\">Release notes</Header>\n                        <p>{data ? data.data.attributes.release_notes.split(\"\\n\").map((sentence: string) => (<span>{sentence}<br /></span>)) : \"\"}</p>\n\n                        <Header as=\"h4\">Minimum OS</Header>\n                        <p>{data ? data.data.attributes.minimum_os_version : \"\"}</p>\n                    </Grid.Column>\n                </Grid>\n\n                <Divider hidden />\n                <Header as=\"h4\">Install to tags</Header>\n                <TagDropdown\n                    loading={tags.loading}\n                    tags={tags.items}\n                    value={appTags}\n                    onAddItem={this.handleAddTag}\n                    onSearch={this.handleSearchTag}\n                    onChange={this.handleChangeTag}\n                />\n\n                <Menu pointing secondary color=\"purple\" inverted>\n                    <MenuItemLink to={`/applications/id/${id}/devices`}>Device Status</MenuItemLink>\n                </Menu>\n\n                <Route path=\"/applications/id/:id/devices\" component={ApplicationDeviceStatus}/>\n            </Container>\n        );\n    }\n\n    protected handleAddTag = (event: SyntheticEvent<MouseEvent>, { value }: { value: string }) => {\n        const tag: Tag = {\n            color:  \"888888\",\n            name: value,\n        };\n\n        this.props.postRelated<Tag>(\"\" + this.props.application.data.data.id, \"tags\", tag);\n    };\n\n    protected handleSearchTag = (value: string) => {\n        this.props.fetchTags(10, 1, [], [{name: \"name\", op: \"ilike\", val: `%${value}%`}]);\n    };\n\n    protected handleChangeTag = (event: React.SyntheticEvent<HTMLElement>, data: DropdownProps): void => {\n        const value = (data.value as string[]);\n\n        const relationships: RelationshipData = value.map((v: string) => {\n            return {id: v, type: \"tags\"};\n        });\n\n        this.props.patchRelationship(\n            \"\" + this.props.match.params.id, \"tags\", relationships);\n    };\n}\n\nexport const ApplicationPage = connect<IStateProps, IDispatchProps>(\n    (state: RootState) => ({\n        application: state.application,\n        tags: state.tags,\n    }),\n    (dispatch: Dispatch) => bindActionCreators({\n        fetchTags,\n        patchRelationship,\n        read,\n    }, dispatch),\n)(UnconnectedApplicationPage);\n"
  },
  {
    "path": "ui/src/containers/ApplicationsPage.tsx",
    "content": "import * as React from \"react\";\nimport {Link, Route} from \"react-router-dom\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {\n    Container,\n    Header,\n    Dropdown,\n    Divider,\n    Breadcrumb,\n} from \"semantic-ui-react\";\n\nimport {RootState} from \"../reducers\";\nimport {ApplicationsTable} from \"../components/react-tables/ApplicationsTable\";\nimport {IApplicationsState} from \"../store/applications/list_reducer\";\nimport {IReactTableState} from \"../store/table/types\";\nimport {FlaskFilterOperation} from \"../flask-rest-jsonapi\";\nimport {index, IndexActionRequest} from \"../store/applications/actions\";\nimport {FlaskFilter} from \"../flask-rest-jsonapi\";\n\nexport interface IDispatchProps {\n    index: IndexActionRequest;\n}\n\nexport interface IStateProps {\n    applications: IApplicationsState;\n}\n\ntype ApplicationsPageProps = IDispatchProps & IStateProps & RouteComponentProps<any>;\n\nclass UnconnectedApplicationsPage extends React.Component<ApplicationsPageProps, any> {\n    public render() {\n        const {\n            applications,\n        } = this.props;\n\n        return (\n            <Container className=\"ApplicationsPage\">\n                <Divider hidden/>\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section>Applications</Breadcrumb.Section>\n                </Breadcrumb>\n                <Divider hidden/>\n\n                <Header as=\"h1\">Applications</Header>\n                <Dropdown text=\"Add\" icon=\"plus\" labeled button className=\"icon\">\n                    <Dropdown.Menu>\n                        <Dropdown.Item as={Link} to=\"/applications/add/macos\">macOS Enterprise Package\n                            (.pkg)</Dropdown.Item>\n                        <Dropdown.Item as={Link} to=\"/applications/add/macSoftware\">macOS App Store\n                            Application</Dropdown.Item>\n                        <Dropdown.Item as={Link} to=\"/applications/add/software\">iOS App Store\n                            Application</Dropdown.Item>\n                        <Dropdown.Item as={Link} to=\"/applications/add/ios\" disabled>iOS Enterprise Application\n                            (.ipa)</Dropdown.Item>\n                    </Dropdown.Menu>\n                </Dropdown>\n\n                <ApplicationsTable data={applications.items}\n                                   loading={applications.loading}\n                                   onFetchData={this.fetchData}\n                />\n            </Container>\n        );\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.index(state.pageSize, state.page, sorting, filtering);\n    }\n}\n\nexport const ApplicationsPage = connect(\n    (state: RootState, ownProps?: any) => ({\n        applications: state.applications,\n    }),\n    (dispatch: Dispatch, ownProps?: any) => bindActionCreators({\n        index,\n    }, dispatch),\n)(UnconnectedApplicationsPage);\n"
  },
  {
    "path": "ui/src/containers/DEPAccountPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {Link} from \"react-router-dom\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {DEPProfilesTable} from \"../components/react-tables/DEPProfilesTable\";\nimport {ButtonLink} from \"../components/semantic-ui/ButtonLink\";\nimport {RootState} from \"../reducers\";\nimport {IDEPAccountState} from \"../store/dep/account_reducer\";\nimport {account, AccountReadActionCreator} from \"../store/dep/actions\";\n\nimport {\n    Breadcrumb,\n    Container,\n    Divider,\n    Header,\n    List,\n    Segment,\n} from \"semantic-ui-react\";\n\nimport {IReactTableState} from \"../store/table/types\";\nimport {FlaskFilterOperation} from \"../flask-rest-jsonapi\";\nimport {FlaskFilter} from \"../flask-rest-jsonapi\";\n\ninterface IReduxStateProps {\n    dep_account?: IDEPAccountState;\n}\n\ninterface IReduxDispatchProps {\n    getAccount: AccountReadActionCreator;\n}\n\ninterface IRouteParameters {\n    id?: string;\n}\n\ninterface IDEPAccountPageProps extends IReduxStateProps, IReduxDispatchProps, RouteComponentProps<IRouteParameters> {\n\n}\n\nclass UnconnectedDEPAccountPage extends React.Component<IDEPAccountPageProps, any> {\n\n    public componentWillMount() {\n        this.props.getAccount(this.props.match.params.id, [\"dep_profiles\"]);\n    }\n\n    public render() {\n\n        const {\n            dep_account: {\n                loading,\n                error,\n                dep_account,\n                dep_profiles,\n            },\n        } = this.props;\n\n        const title = (dep_account && !loading) ? dep_account.attributes.server_name : \"DEP Account (loading)\";\n\n        return (\n            <Container className=\"DEPAccountPage\">\n                <Divider hidden />\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/settings`}>Settings</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/settings/dep/accounts`}>DEP Accounts</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active>{loading ? \"DEP Account\" : title}</Breadcrumb.Section>\n                </Breadcrumb>\n\n                <Header as=\"h1\">{title}</Header>\n\n                {dep_account && !loading &&\n                <div>\n                    <Segment.Group>\n                        <Segment attached>\n                          <Header as=\"h3\">Overview</Header>\n                          <List>\n                            <List.Item>\n                              <List.Content>\n                                <List.Header>Server Name (As shown in Apple School Manager or Apple Business\n                                  Manager)</List.Header>\n                                <List.Description>{dep_account.attributes.server_name}</List.Description>\n                              </List.Content>\n                            </List.Item>\n                            <List.Item>\n                              <List.Content>\n                                <List.Header>Administrator Apple ID</List.Header>\n                                <List.Description>{dep_account.attributes.admin_id}</List.Description>\n                              </List.Content>\n                            </List.Item>\n                          </List>\n                        </Segment>\n                        <Segment attached>\n                            <Header as=\"h3\">Organization</Header>\n                            <List>\n                              <List.Item icon=\"building\" description={dep_account.attributes.org_name}/>\n                              <List.Item icon=\"compass\" description={dep_account.attributes.org_address}/>\n                              <List.Item icon=\"mail\" description={dep_account.attributes.org_email}/>\n                              <List.Item icon=\"mobile\" description={dep_account.attributes.org_phone}/>\n                            </List>\n                        </Segment>\n                    </Segment.Group>\n\n                  <Header as=\"h3\">Profiles</Header>\n                  <ButtonLink to={`/dep/accounts/${dep_account.id}/add/profile`}>New DEP Profile</ButtonLink>\n\n                  <DEPProfilesTable\n                    data={dep_profiles}\n                  />\n                </div>\n                }\n            </Container>\n        );\n    }\n}\n\nexport const DEPAccountPage = connect<IReduxStateProps, IReduxDispatchProps, any>(\n    (state: RootState, ownProps: any): IReduxStateProps => {\n        return {dep_account: state.dep.account};\n    },\n    (dispatch: Dispatch, ownProps?: any): IReduxDispatchProps => bindActionCreators({\n        getAccount: account,\n    }, dispatch),\n)(UnconnectedDEPAccountPage);\n"
  },
  {
    "path": "ui/src/containers/DEPProfilePage.tsx",
    "content": "import * as React from \"react\";\n\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {Link} from \"react-router-dom\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {\n    AccordionTitleProps,\n    Breadcrumb,\n    Container,\n    Header,\n    Divider,\n} from \"semantic-ui-react\";\n\nimport {DEPProfileForm, IDEPProfileFormValues} from \"../components/forms/DEPProfileForm\";\nimport {RSAAApiErrorMessage} from \"../components/RSAAApiErrorMessage\";\nimport {RootState} from \"../reducers\";\nimport {\n    patchProfile,\n    postProfile,\n    profile, ProfilePatchActionRequest,\n    ProfilePostActionCreator,\n    ProfileReadActionCreator,\n} from \"../store/dep/actions\";\nimport {IDEPProfileState} from \"../store/dep/profile_reducer\";\nimport {DEPProfile, SkipSetupSteps} from \"../store/dep/types\";\nimport {JSONAPIDataObject} from \"../store/json-api\";\n\ninterface IReduxStateProps {\n    dep_profile?: IDEPProfileState;\n}\n\ninterface IReduxDispatchProps {\n    getDEPProfile: ProfileReadActionCreator;\n    postDEPProfile: ProfilePostActionCreator;\n    patchDEPProfile: ProfilePatchActionRequest;\n}\n\ninterface IRouteParameters {\n    account_id: string;\n    id?: string;\n}\n\ninterface IDEPProfilePageProps extends IReduxStateProps, IReduxDispatchProps, RouteComponentProps<IRouteParameters> {\n\n}\n\ninterface IDEPProfilePageState {\n    activeIndex: string | number;\n}\n\nclass UnconnectedDEPProfilePage extends React.Component<IDEPProfilePageProps, IDEPProfilePageState> {\n\n    constructor(props: IDEPProfilePageProps) {\n        super(props);\n        this.state = {\n            activeIndex: 0,\n        };\n    }\n\n    public componentWillMount() {\n        const {\n            match: {\n                params: {\n                    id,\n                },\n            },\n        } = this.props;\n\n        if (id) {\n            this.props.getDEPProfile(this.props.match.params.id);\n        }\n    }\n\n    public render() {\n        const {\n            dep_profile,\n            match: {\n                params: {\n                    id,\n                    account_id,\n                },\n            },\n        } = this.props;\n\n        let title = \"loading\";\n        let breadcrumbTitle = \"DEP Profile\";\n        let formValues: IDEPProfileFormValues = {};\n\n        if (id) {\n            title = `Edit ${this.props.dep_profile.dep_profile ?\n                this.props.dep_profile.dep_profile.attributes.profile_name : \"Loading...\"}`;\n\n            if (dep_profile.dep_profile) {\n                breadcrumbTitle = dep_profile.dep_profile.attributes.profile_name;\n                formValues = this.profileToForm(dep_profile.dep_profile);\n            }\n        } else {\n            title = \"Create a new DEP Profile\";\n        }\n\n        return (\n            <Container className=\"DEPProfilePage\">\n                <Divider hidden />\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/dep/accounts/${account_id}`}>DEP Account</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active>{ breadcrumbTitle }</Breadcrumb.Section>\n                </Breadcrumb>\n\n                <Header as=\"h1\">{title}</Header>\n                {dep_profile.error && <RSAAApiErrorMessage error={dep_profile.errorDetail} />}\n                <DEPProfileForm onSubmit={this.handleSubmit}\n                                loading={dep_profile.loading}\n                                isSubmitting={dep_profile.loading}\n                                data={formValues}\n                                id={dep_profile.dep_profile && dep_profile.dep_profile.id}\n                                activeIndex={this.state.activeIndex}\n                                onClickAccordionTitle={this.handleAccordionClick}\n                />\n            </Container>\n        );\n    }\n\n    private handleSubmit = (values: IDEPProfileFormValues) => {\n        const profile = this.formToProfile(values);\n\n        if (this.props.match.params.id) {\n            this.props.patchDEPProfile(this.props.match.params.id, profile);\n        } else {\n            this.props.postDEPProfile(profile, {\n                dep_account: {type: \"dep_accounts\", id: this.props.match.params.account_id},\n            });\n        }\n    };\n\n    private handleAccordionClick = (event: React.MouseEvent<any>, data: AccordionTitleProps) => {\n        this.setState({ activeIndex: data.index });\n    };\n\n    private formToProfile = (formValues: IDEPProfileFormValues): DEPProfile => {\n        const skipSetupSteps: SkipSetupSteps[] = [];\n        const { show, ...attrs } = formValues;\n\n        for (const kskip in SkipSetupSteps) {\n            if (SkipSetupSteps.hasOwnProperty(kskip)) {\n                if (!show[kskip]) {\n                    skipSetupSteps.unshift(kskip as SkipSetupSteps);\n                }\n            }\n        }\n\n        return {\n            ...attrs,\n            skip_setup_items: skipSetupSteps,\n        };\n    };\n\n    private profileToForm = (profile: JSONAPIDataObject<DEPProfile>): IDEPProfileFormValues => {\n        const show: { [propName: string]: boolean } = {};\n        const { skip_setup_items, ...attrs } = profile.attributes;\n\n        for (const kskip in SkipSetupSteps) {\n            if (SkipSetupSteps.hasOwnProperty(kskip)) {\n                show[kskip] = skip_setup_items.indexOf(kskip as SkipSetupSteps) === -1;\n            }\n        }\n        return {\n            ...attrs,\n            show,\n        };\n    }\n\n}\n\nexport const DEPProfilePage = connect(\n    (state: RootState, ownProps: any): IReduxStateProps => {\n        return {dep_profile: state.dep.profile};\n    },\n    (dispatch: Dispatch, ownProps?: any): IReduxDispatchProps => bindActionCreators({\n        getDEPProfile: profile,\n        patchDEPProfile: patchProfile,\n        postDEPProfile: postProfile,\n    }, dispatch),\n)(UnconnectedDEPProfilePage);\n"
  },
  {
    "path": "ui/src/containers/DashboardPage.tsx",
    "content": "import * as React from \"react\";\nimport {Container} from \"semantic-ui-react\";\nimport {RouteComponentProps} from \"react-router\";\n\nexport const DashboardPage: React.FunctionComponent<RouteComponentProps<any>> = () => (\n    <Container className=\"DashboardPage\">\n        <ul>\n            <li><a href=\"/enroll/profile\">Enroll (Direct)</a></li>\n            <li><a href=\"/enroll/ota\">Enroll (OTA)</a></li>\n            <li><a href=\"/enroll/trust.mobileconfig\">Download Trust Profile</a></li>\n        </ul>\n    </Container>\n);\n"
  },
  {
    "path": "ui/src/containers/DevicePage.tsx",
    "content": "import * as React from \"react\";\nimport {SyntheticEvent} from \"react\";\nimport {connect} from \"react-redux\";\n\nimport {RouteComponentProps} from \"react-router\";\nimport {Link} from \"react-router-dom\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {MacOSDeviceDetail} from \"../components/devices/MacOSDeviceDetail\";\nimport {isArray} from \"../guards\";\nimport {RootState} from \"../reducers/index\";\nimport {getPercentCapacityUsed} from \"../selectors/device\";\nimport {\n    CacheFetchActionRequest,\n    clearPasscode,\n    ClearPasscodeActionRequest,\n    fetchDeviceIfRequired,\n    inventory,\n    InventoryActionRequest,\n    lock,\n    LockActionRequest,\n    patchRelationship,\n    PatchRelationshipActionRequest,\n    postRelated,\n    PostRelatedActionRequest,\n    push,\n    PushActionRequest,\n    restart,\n    RestartActionRequest,\n    shutdown,\n    ShutdownActionRequest,\n} from \"../store/device/actions\";\nimport {DeviceState} from \"../store/device/reducer\";\nimport {JSONAPIRelationship, JSONAPIResourceIdentifier} from \"../store/json-api\";\nimport {\n    index as fetchTags,\n    IndexActionRequest,\n    post as createTag,\n    PostActionRequest as PostTagActionRequest,\n} from \"../store/tags/actions\";\nimport {ITagsState} from \"../store/tags/reducer\";\nimport {Tag} from \"../store/tags/types\";\n\nimport {Breadcrumb, Container, Divider, DropdownProps} from \"semantic-ui-react\";\n\nimport {DEPDeviceDetail} from \"../components/devices/DEPDeviceDetail\";\nimport {IOSDeviceDetail} from \"../components/devices/IOSDeviceDetail\";\nimport {DeviceOperatingSystem, operatingSystem} from \"../store/device/types\";\n\ninterface IReduxStateProps {\n    device: DeviceState;\n    tags: ITagsState;\n    percentCapacityUsed: number;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): IReduxStateProps {\n    return {\n        device: state.device,\n        percentCapacityUsed: getPercentCapacityUsed(state),\n        tags: state.tags,\n    };\n}\n\ninterface IReduxDispatchProps {\n    clearPasscode: ClearPasscodeActionRequest;\n    createTag: PostTagActionRequest;\n    fetchDeviceIfRequired: CacheFetchActionRequest;\n    fetchTags: IndexActionRequest;\n    inventory: InventoryActionRequest;\n    lock: LockActionRequest;\n    patchRelationship: PatchRelationshipActionRequest;\n    postRelated: PostRelatedActionRequest;\n    push: PushActionRequest;\n    restart: RestartActionRequest;\n    shutdown: ShutdownActionRequest;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch, ownProps?: any): IReduxDispatchProps {\n    return bindActionCreators({\n        clearPasscode,\n        createTag,\n        fetchDeviceIfRequired,\n        fetchTags,\n        inventory,\n        lock,\n        patchRelationship,\n        postRelated,\n        push,\n        restart,\n        shutdown,\n    }, dispatch);\n}\n\ninterface IRouteParameters {\n    id?: string;\n}\n\ntype DevicePageProps = IReduxStateProps & IReduxDispatchProps & RouteComponentProps<IRouteParameters>;\n\ninterface IDevicePageState {\n    filter: string;\n}\n\nclass BaseDevicePage extends React.Component<DevicePageProps, IDevicePageState> {\n\n    public componentDidMount(): void {\n        this.props.fetchDeviceIfRequired(\"\" + this.props.match.params.id, [\"tags\"]);\n        this.props.fetchTags(40);\n    }\n\n    public render(): JSX.Element {\n        const {\n            device,\n            match: {params: {id: device_id}},\n            tags,\n\n            clearPasscode,\n            inventory,\n            lock,\n            push,\n            restart,\n            shutdown,\n        } = this.props;\n\n        let deviceTags: number[] = [];\n        if (device.device && device.device.relationships && device.device.relationships.tags) {\n            if (isArray(device.device.relationships.tags.data)) {\n                deviceTags = device.device.relationships.tags.data.map((t: JSONAPIResourceIdentifier) => parseInt(t.id, 0));\n            } else {\n                deviceTags = [parseInt(device.device.relationships.tags.data.id, 0)];\n            }\n        }\n\n        let DetailComponent = <span>Loading</span>;\n        let showTools = true;\n\n        const actions = {\n            clearPasscode,\n            inventory,\n            lock,\n            push,\n            restart,\n            shutdown,\n        };\n\n        if (device.device && !device.loading) {\n            if (device.device.attributes.is_dep && device.device.attributes.is_enrolled === false) {\n                DetailComponent = <DEPDeviceDetail device={device}/>;\n                showTools = false;\n            } else if (operatingSystem(device.device.attributes.model_name) === DeviceOperatingSystem.iOS) {\n                DetailComponent = <IOSDeviceDetail device={device}\n                                                   tags={tags}\n                                                   deviceTags={deviceTags}\n                                                   onAddTag={this.handleAddTag}\n                                                   onChangeTag={this.handleChangeTag}\n                                                   onSearchTag={this.handleSearchTag}\n                                                   {...actions} />;\n            } else {\n                DetailComponent = <MacOSDeviceDetail device={device}\n                                                     tags={tags}\n                                                     deviceTags={deviceTags}\n                                                     onAddTag={this.handleAddTag}\n                                                     onChangeTag={this.handleChangeTag}\n                                                     onSearchTag={this.handleSearchTag}\n                                                     {...actions} />;\n            }\n        }\n\n        return (\n            <Container className=\"DevicePage\">\n                <Divider hidden />\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/devices`}>Devices</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section>{device.device ? device.device.attributes.device_name : \"Device\"}</Breadcrumb.Section>\n                </Breadcrumb>\n\n                {DetailComponent}\n\n            </Container>\n        );\n    }\n\n    protected handleAddTag = (event: SyntheticEvent<MouseEvent>, { value }: { value: string }) => {\n        const tag: Tag = {\n            color:  \"888888\",\n            name: value,\n        };\n\n        this.props.postRelated<Tag>(\"\" + this.props.device.device.id, \"tags\", tag);\n    };\n\n    protected handleSearchTag = (value: string) => {\n        this.props.fetchTags(10, 1, [], [{name: \"name\", op: \"ilike\", val: `%${value}%`}]);\n     };\n\n    protected handleChangeTag = (event: React.SyntheticEvent<HTMLElement>, data: DropdownProps): void => {\n        const value = (data.value as string[]);\n\n        const relationships = value.map((v: string) => {\n            return {id: v, type: \"tags\"};\n        });\n\n        this.props.patchRelationship(\n            \"\" + this.props.match.params.id, \"tags\", relationships);\n    };\n}\n\nexport const DevicePage = connect<IReduxStateProps, IReduxDispatchProps, DevicePageProps>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(BaseDevicePage);\n"
  },
  {
    "path": "ui/src/containers/DeviceRename.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {DeviceRenameModal} from \"../components/modals/DeviceRenameModal\";\nimport {RootState} from \"../reducers\";\nimport {upload, UploadActionRequest} from \"../store/profiles/actions\";\n\nexport interface IReduxStateProps {\n\n}\n\nexport interface IReduxDispatchProps {\n\n}\n\nexport const DeviceRename = connect<IReduxStateProps, IReduxDispatchProps>(\n    (state: RootState) => {\n        return state.device;\n    },\n    (dispatch: Dispatch, ownProps: any) => bindActionCreators({\n        upload,\n    }, dispatch),\n)(DeviceRenameModal);\n"
  },
  {
    "path": "ui/src/containers/DevicesPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {\n    Grid,\n    Container,\n    Header,\n    Divider\n} from \"semantic-ui-react\";\n\n\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Store, Dispatch} from \"redux\";\nimport {SUISelectionTools} from \"../components/react-table/SUISelectionTools\";\nimport {DevicesTable} from \"../components/react-tables/DevicesTable\";\nimport {RootState} from \"../reducers/index\";\nimport {FlaskFilterOperation} from \"../flask-rest-jsonapi\";\nimport * as actions from \"../store/device/actions\";\nimport {IDevicesState} from \"../store/devices/devices\";\nimport * as tableActions from \"../store/table/actions\";\nimport {ToggleSelectionActionCreator} from \"../store/table/actions\";\nimport {ITableState} from \"../store/table/reducer\";\nimport {IReactTableState} from \"../store/table/types\";\nimport * as tagActions from \"../store/tags/actions\";\nimport {ITagsState} from \"../store/tags/reducer\";\nimport {FlaskFilter} from \"../flask-rest-jsonapi\";\n\ninterface IReduxStateProps {\n    devices: IDevicesState;\n    table: ITableState;\n    tags: ITagsState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): IReduxStateProps {\n    return {\n        devices: state.devices,\n        table: state.table,\n        tags: state.tags,\n    };\n}\n\ninterface IReduxDispatchProps {\n    fetchDevicesIfRequired: any;\n    index: actions.IndexActionRequest;\n    tagIndex: tagActions.IndexActionRequest;\n    toggleSelection: ToggleSelectionActionCreator;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch, ownProps?: any): IReduxDispatchProps {\n    return bindActionCreators({\n        fetchDevicesIfRequired: actions.fetchDevicesIfRequired,\n        index: actions.index,\n        tagIndex: tagActions.index,\n        toggleSelection: tableActions.toggleSelection,\n    }, dispatch);\n}\n\ntype DevicesPageProps = IReduxStateProps & IReduxDispatchProps & RouteComponentProps<any>;\n\nclass UnconnectedDevicesPage extends React.Component<DevicesPageProps, any> {\n\n    public componentWillMount?(): void {\n        // this.props.index();\n        this.props.fetchDevicesIfRequired();\n        this.props.tagIndex();\n    }\n\n    public render(): JSX.Element {\n        const {\n            devices,\n            toggleSelection,\n            table,\n            tags,\n        } = this.props;\n\n        return (\n            <Container className=\"DevicesPage\">\n                <Divider hidden />\n                <Header as=\"h1\">Devices</Header>\n\n                <SUISelectionTools loading={devices.loading || tags.loading}\n                                   tags={tags.items} selectionCount={table.selection.length} />\n                <DevicesTable\n                    data={devices.items}\n                    loading={devices.loading}\n                    toggleSelection={toggleSelection}\n                    isSelected={(key) => table.selection.indexOf(key) !== -1}\n                    onFetchData={this.fetchData}\n                />\n            </Container>\n        );\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.index(state.pageSize, state.page, sorting, filtering);\n    }\n}\n\nexport const DevicesPage = connect<IReduxStateProps, IReduxDispatchProps, DevicesPageProps>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(UnconnectedDevicesPage);\n"
  },
  {
    "path": "ui/src/containers/LoginPage.tsx",
    "content": "import * as React from \"react\";\n\nimport {connect} from \"react-redux\";\nimport {Redirect, RouteComponentProps, RouteProps} from \"react-router\";\nimport {Grid, Card, Checkbox, Form, Header, Input, Message} from \"semantic-ui-react\";\nimport * as actions from \"../store/auth/actions\";\nimport {\n    Formik,\n    FormikActions,\n    FormikProps,\n    Form as FormikForm,\n    Field,\n    FieldProps,\n    Label,\n    withFormik,\n    FormikBag\n} from \"formik\";\nimport { Button } from \"formik-semantic-ui\";\nimport {RootState} from \"../reducers\";\nimport {ApiError} from \"redux-api-middleware\";\nimport * as Yup from \"yup\";\n\ninterface IFormValues {\n    email: string;\n    password: string;\n}\n\ninterface IReduxDispatchProps {\n    login: actions.TokenActionRequestCreator;\n}\n\nexport interface ILoginPageProps extends IReduxDispatchProps {\n    apiError?: ApiError;\n    access_token?: string;\n    loading: boolean;\n}\n\nexport interface IRouteProps {\n    from?: string;\n}\n\nconst UnconnectedLoginPage: React.FC = (\n    props: ILoginPageProps & RouteComponentProps<IRouteProps> & FormikProps<IFormValues>) => {\n    const { touched, errors, isSubmitting, handleBlur, handleChange, values, handleSubmit, login, loading, apiError,\n        access_token } = props;\n\n    return (\n        <Grid textAlign=\"center\" style={{height: '100vh'}} verticalAlign=\"middle\">\n            {access_token ? <Redirect to={{\n                pathname: \"/\",\n            }}/> :\n            <Grid.Column style={{maxWidth: 450}}>\n                <Card>\n                    <Card.Content>\n                        <Card.Header>Sign in</Card.Header>\n                        <Card.Description>\n                            <Form size=\"large\" onSubmit={handleSubmit}>\n                                <Form.Field required>\n                                    <Input\n                                        name=\"email\"\n                                        fluid\n                                        icon=\"user\"\n                                        iconPosition=\"left\"\n                                        placeholder=\"E-mail address\"\n                                        onChange={handleChange} onBlur={handleBlur}\n                                        value={values.email}\n                                    />\n                                    {errors.email &&\n                                    touched.email &&\n                                    <Label pointing>{errors.email}</Label>}\n                                </Form.Field>\n                                <Form.Field required>\n                                    <Input\n                                        name=\"password\"\n                                        fluid\n                                        icon=\"lock\"\n                                        iconPosition=\"left\"\n                                        placeholder=\"Password\"\n                                        type=\"password\"\n                                        onChange={handleChange} onBlur={handleBlur}\n                                        value={values.password}\n                                    />\n                                    {errors.password &&\n                                    touched.password &&\n                                    <Label pointing>{errors.password}</Label>}\n                                </Form.Field>\n                                <Button.Submit color=\"violet\" fluid size=\"large\" type=\"submit\" loading={loading}>\n                                    Submit\n                                </Button.Submit>\n\n                                {apiError && <Message color=\"red\">\n                                    {apiError.response.error_description}\n                                </Message>}\n                            </Form>\n                        </Card.Description>\n                    </Card.Content>\n                </Card>\n            </Grid.Column>}\n        </Grid>\n    );\n};\n\nconst UnconnectedLoginPageForm = withFormik({\n    handleSubmit: (values, formikBag: FormikBag<ILoginPageProps, IFormValues>) => {\n        formikBag.props.login(values.email, values.password);\n        formikBag.setSubmitting(false);\n    },\n\n    validationSchema: Yup.object().shape({\n        email: Yup.string().required(\"Required\"),\n        password: Yup.string().required(\"Required\"),\n    }),\n})(UnconnectedLoginPage);\n\nexport const LoginPage = connect(\n    (state: RootState) => ({\n        access_token: state.auth.access_token,\n        apiError: state.auth.error,\n        loading: state.auth.loading,\n    }),\n    {login: actions.login})(UnconnectedLoginPageForm);\n"
  },
  {
    "path": "ui/src/containers/LogoutPage.tsx",
    "content": "import * as React from \"react\";\n\nimport {connect} from \"react-redux\";\nimport {Redirect, RouteComponentProps, RouteProps} from \"react-router\";\nimport {Grid, Card, Checkbox, Form, Header, Input, Message} from \"semantic-ui-react\";\nimport * as actions from \"../store/auth/actions\";\nimport {\n    Formik,\n    FormikActions,\n    FormikProps,\n    Form as FormikForm,\n    Field,\n    FieldProps,\n    Label,\n    withFormik,\n    FormikBag\n} from \"formik\";\nimport { Button } from \"formik-semantic-ui\";\nimport {RootState} from \"../reducers\";\nimport {ApiError} from \"redux-api-middleware\";\nimport * as Yup from \"yup\";\n\ninterface IFormValues {\n    email: string;\n    password: string;\n}\n\ninterface IReduxDispatchProps {\n    logout: actions.TokenActionRequestCreator;\n}\n\nexport interface ILogoutPageProps extends IReduxDispatchProps {\n    apiError?: ApiError;\n    access_token?: string;\n    loading: boolean;\n}\n\nexport interface IRouteProps {\n    from?: string;\n}\n\nconst UnconnectedLogoutPage: React.FC = (\n    props: ILogoutPageProps & RouteComponentProps<IRouteProps>) => {\n\n    return (\n        <Grid textAlign=\"center\" style={{height: '100vh'}} verticalAlign=\"middle\">\n            <Grid.Column style={{maxWidth: 450}}>\n                <Card>\n                    <Card.Content>\n                        <Card.Header>Logging Out</Card.Header>\n                        <Card.Description>\n                            Hold on while we log you out...\n                        </Card.Description>\n                    </Card.Content>\n                </Card>\n            </Grid.Column>}\n        </Grid>\n    );\n};\n\nexport const LogoutPage = connect(\n    (state: RootState) => ({\n        access_token: state.auth.access_token,\n        apiError: state.auth.error,\n        loading: state.auth.loading,\n    }),\n    {Logout: actions.logout})(UnconnectedLogoutPage);\n"
  },
  {
    "path": "ui/src/containers/ProfilePage.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {RootState} from \"../reducers/index\";\n\nimport {Container, Header, Breadcrumb, Divider} from \"semantic-ui-react\";\nimport { DropdownProps } from \"semantic-ui-react\";\n\nimport {SyntheticEvent} from \"react\";\nimport {RouteComponentProps} from \"react-router\";\nimport {TagDropdown} from \"../components/TagDropdown\";\nimport {isArray} from \"../guards\";\nimport {JSONAPIDataObject, JSONAPIRelationship} from \"../store/json-api\";\nimport {patchRelationship, PatchRelationshipActionRequest} from \"../store/profiles/actions\";\nimport {read, ReadActionRequest} from \"../store/profiles/actions\";\nimport {Profile} from \"../store/profiles/types\";\nimport {\n    index as fetchTags, IndexActionRequest,\n    post as createTag, PostActionRequest as PostTagActionRequest,\n} from \"../store/tags/actions\";\nimport {ITagsState} from \"../store/tags/reducer\";\nimport {Tag} from \"../store/tags/types\";\n\nimport {Link} from \"react-router-dom\";\nimport {ResourceIdentifier} from \"../json-api-v1\";\n\ninterface IRouteProps {\n    id?: string;\n}\n\ninterface IReduxStateProps {\n    profile?: JSONAPIDataObject<Profile>;\n    tags: ITagsState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): IReduxStateProps {\n    return {\n        profile: state.profile.profile,\n        tags: state.tags,\n    };\n}\n\ninterface IReduxDispatchProps {\n    read: ReadActionRequest;\n    fetchTags: IndexActionRequest;\n    createTag: PostTagActionRequest;\n    patchRelationship: PatchRelationshipActionRequest;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch): IReduxDispatchProps {\n    return bindActionCreators({\n        createTag,\n        fetchTags,\n        patchRelationship,\n        read,\n    }, dispatch);\n}\n\nexport class UnconnectedProfilePage extends React.Component<RouteComponentProps<IRouteProps> & IReduxStateProps & IReduxDispatchProps, void | {}> {\n\n    public componentWillMount?() {\n        const {params: {id}} = this.props.match;\n        this.props.read(id, [\"tags\"]);\n        this.props.fetchTags();\n    }\n\n    public render() {\n        const {\n            profile,\n            tags,\n        } = this.props;\n\n        let profileTags: number[] = [];\n        if (profile && profile.relationships && profile.relationships.tags) {\n            if (isArray(profile.relationships.tags.data)) {\n                profileTags = profile.relationships.tags.data.map((t: ResourceIdentifier) => parseInt(t.id, 0));\n            }\n\n        }\n\n        return (\n            <Container className=\"ProfilePage\">\n                <Divider hidden />\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/profiles`}>Profiles</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section>Profile</Breadcrumb.Section>\n                </Breadcrumb>\n\n                <Header as=\"h1\">\n                    {profile && profile.attributes.display_name}\n                    <Header.Subheader>{profile && profile.attributes.uuid}</Header.Subheader>\n                    <Header.Subheader>{profile && profile.attributes.identifier}</Header.Subheader>\n                </Header>\n                <TagDropdown\n                    loading={tags.loading}\n                    tags={tags.items}\n                    value={profileTags}\n                    onAddItem={this.handleAddTag}\n                    onSearch={this.handleSearchTag}\n                    onChange={this.handleChangeTag}\n                />\n\n            </Container>\n        );\n    }\n\n    private handleAddTag = (event: SyntheticEvent<MouseEvent>, {value}: { value: string }) => {\n        const tag: Tag = {\n            color: \"888888\",\n            name: value,\n        };\n\n        this.props.createTag(tag);\n    };\n\n    private handleSearchTag = (value: string) => {\n        this.props.fetchTags(10, 1, [], [{name: \"name\", op: \"ilike\", val: `%${value}%`}]);\n    };\n\n    private handleChangeTag = (event: React.SyntheticEvent<HTMLElement>, data: DropdownProps): void => {\n        const { value } = data;\n\n        const relationships = value.map((v: string) => {\n            return {id: v, type: \"tags\"};\n        });\n\n        this.props.patchRelationship(\n            \"\" + this.props.match.params.id, \"tags\", relationships);\n    };\n}\n\nexport const ProfilePage = connect<IReduxStateProps, IReduxDispatchProps>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(UnconnectedProfilePage);\n"
  },
  {
    "path": "ui/src/containers/ProfileUpload.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {ProfileUploadModal} from \"../components/modals/ProfileUploadModal\";\nimport {RootState} from \"../reducers\";\nimport {upload, UploadActionRequest} from \"../store/profiles/actions\";\n\nexport interface IReduxDispatchProps {\n    upload: UploadActionRequest;\n}\n\nexport const ProfileUpload = connect<any, IReduxDispatchProps>(\n    (state: RootState) => {\n        return state.profiles;\n    },\n    (dispatch: Dispatch, ownProps: any) => bindActionCreators({\n        upload,\n    }, dispatch),\n)(ProfileUploadModal);\n"
  },
  {
    "path": "ui/src/containers/ProfilesPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {Dispatch} from \"redux\";\n\nimport {Container, Header, Divider, Dropdown} from \"semantic-ui-react\";\n\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators} from \"redux\";\nimport {RootState} from \"../reducers/index\";\nimport * as actions from \"../store/profiles/actions\";\nimport {IndexActionRequest, UploadActionRequest} from \"../store/profiles/actions\";\nimport {ProfilesState} from \"../store/profiles/reducer\";\nimport {ProfilesTable} from \"../components/react-tables/ProfilesTable\";\nimport {Link} from \"react-router-dom\";\nimport {ToggleSelectionActionCreator} from \"../store/table/actions\";\nimport * as tableActions from \"../store/table/actions\";\nimport {ITableState} from \"../store/table/reducer\";\nimport {IReactTableState} from \"../store/table/types\";\nimport {FlaskFilterOperation} from \"../flask-rest-jsonapi\";\nimport {FlaskFilter} from \"../flask-rest-jsonapi\";\n\ninterface IReduxStateProps {\n    profiles: ProfilesState;\n    table: ITableState;\n}\n\ninterface IReduxDispatchProps {\n    index: IndexActionRequest;\n    upload: UploadActionRequest;\n    toggleSelection: ToggleSelectionActionCreator;\n}\n\ninterface IProfilesPageProps extends IReduxStateProps, IReduxDispatchProps, RouteComponentProps<any> {\n    componentWillMount: () => void;\n}\n\ninterface IProfilesPageState {\n    filter: string;\n}\n\nexport class UnconnectedProfilesPage extends React.Component<IProfilesPageProps, IProfilesPageState> {\n\n    public componentWillMount?() {\n        this.props.index();\n    }\n\n    public render(): JSX.Element {\n        const {\n            profiles,\n            toggleSelection,\n            table,\n        } = this.props;\n\n        return (\n            <Container className=\"ProfilesPage\">\n                <Divider hidden />\n                <Header as=\"h1\">Profiles</Header>\n                <Dropdown text=\"Add\" icon=\"plus\" labeled button className=\"icon\">\n                    <Dropdown.Menu>\n                        <Dropdown.Item as={Link} to=\"/profiles/add/custom\">Custom Profile (.mobileconfig)</Dropdown.Item>\n                    </Dropdown.Menu>\n                </Dropdown>\n                <Divider hidden />\n                <ProfilesTable\n                    data={profiles.items}\n                    loading={profiles.loading}\n                    toggleSelection={toggleSelection}\n                    isSelected={(key: string) => table.selection.indexOf(key) !== -1}\n                    onFetchData={this.fetchData}\n                />\n            </Container>\n        );\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.index(state.pageSize, state.page, sorting, filtering);\n    }\n}\n\nexport const ProfilesPage = connect<IReduxStateProps, IReduxDispatchProps, IProfilesPageProps>(\n    (state: RootState, ownProps?: any): IReduxStateProps => ({\n        profiles: state.profiles,\n        table: state.table,\n    }),\n    (dispatch: Dispatch, ownProps?: any): IReduxDispatchProps => bindActionCreators({\n        index: actions.index,\n        toggleSelection: tableActions.toggleSelection,\n        upload: actions.upload,\n    }, dispatch),\n)(UnconnectedProfilesPage);\n"
  },
  {
    "path": "ui/src/containers/SettingsPage.tsx",
    "content": "import * as React from \"react\";\nimport {Link, RouteComponentProps} from \"react-router-dom\";\n\nimport {\n    Divider,\n    Container,\n    Header,\n    Icon,\n    Card,\n} from \"semantic-ui-react\";\n\nexport const SettingsPage: React.FunctionComponent<any> = () => (\n    <Container>\n        <Divider hidden/>\n        <Header>General</Header>\n        <Card.Group>\n            <Card as={Link} to=\"/settings/organization\">\n                <Card.Content>\n                    <Card.Header>\n                        <Icon name=\"building\" /> Organization\n                    </Card.Header>\n                    <Card.Description>\n                        Configure your organization\n                    </Card.Description>\n                </Card.Content>\n            </Card>\n            <Card as={Link} to=\"/settings/deviceauth\">\n                <Card.Content>\n                    <Card.Header>\n                        <Icon name=\"protect\" /> Device Authentication\n                    </Card.Header>\n                    <Card.Description>\n                        Configure how communication is secured between your devices and this MDM\n                    </Card.Description>\n                </Card.Content>\n            </Card>\n            <Card as={Link} to=\"/settings/apns\">\n                <Card.Content>\n                    <Card.Header>\n                        <Icon name=\"cloud upload\" /> Push Certificate\n                    </Card.Header>\n                    <Card.Description>\n                        Configure a Push Certificate\n                    </Card.Description>\n                </Card.Content>\n            </Card>\n            <Card as={Link} to=\"/settings/authentication\">\n                <Card.Content>\n                    <Card.Header>\n                        <Icon name=\"users\" /> Authentication\n                    </Card.Header>\n                    <Card.Description>\n                        Configure authentication sources\n                    </Card.Description>\n                </Card.Content>\n            </Card>\n\n        </Card.Group>\n        <Header>Enrollment</Header>\n        <Card.Group>\n            <Card as={Link} to=\"/settings/vpp\">\n                <Card.Content>\n                    <Card.Header>\n                        <Icon name=\"credit card alternative\" /> VPP Accounts\n                    </Card.Header>\n                    <Card.Description>\n                        Configure access to the Volume Purchasing Programme\n                    </Card.Description>\n                </Card.Content>\n            </Card>\n            <Card as={Link} to=\"/settings/dep/accounts\">\n                <Card.Content>\n                    <Card.Header>\n                        <Icon name=\"tablet\" /> DEP Accounts\n                    </Card.Header>\n                    <Card.Description>\n                        Configure the Device Enrollment Programme\n                    </Card.Description>\n                </Card.Content>\n            </Card>\n        </Card.Group>\n    </Container>\n);\n"
  },
  {
    "path": "ui/src/containers/applications/ApplicationDeviceStatus.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {AppDeployStatusTable} from \"../../components/react-tables/AppDeployStatusTable\";\nimport {RootState} from \"../../reducers\";\nimport {devices, DevicesActionRequest, index, IndexActionRequest} from \"../../store/applications/managed\";\nimport {IManagedApplicationsState} from \"../../store/applications/managed_reducer\";\nimport {FlaskFilterOperation} from \"../../flask-rest-jsonapi\";\nimport {IReactTableState} from \"../../store/table/types\";\nimport {managed, ManagedActionRequest} from \"../../store/applications/actions\";\nimport {FlaskFilter} from \"../../flask-rest-jsonapi\";\n\ninterface IDispatchProps {\n    index: IndexActionRequest;\n    devices: DevicesActionRequest;\n    managed: ManagedActionRequest;\n}\n\ninterface IStateProps {\n    store: IManagedApplicationsState;\n}\n\ninterface IApplicationDeviceStatusRouteProps {\n    id?: string;\n}\n\nexport type IApplicationDeviceStatusProps = IDispatchProps & IStateProps &\n    RouteComponentProps<IApplicationDeviceStatusRouteProps>;\n\nclass UnconnectedApplicationDeviceStatus extends React.Component<IApplicationDeviceStatusProps, void> {\n\n    public render() {\n        const { store } = this.props;\n\n        return (\n            <div className=\"ApplicationDeviceStatus\">\n                <AppDeployStatusTable\n                    data={store.items}\n                    defaultPageSize={store.pageSize}\n                    loading={store.loading}\n                    onFetchData={this.fetchData}\n                    pages={store.pages}\n                    />\n            </div>\n        )\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const appId = this.props.match.params.id;\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.managed(appId, state.pageSize, state.page + 1, sorting, filtering);\n    }\n}\n\nexport const ApplicationDeviceStatus = connect(\n    (state: RootState) => ({\n        store: state.managed_applications,\n}),\n    (dispatch: Dispatch) => bindActionCreators({\n        devices,\n        index,\n        managed,\n}, dispatch))(UnconnectedApplicationDeviceStatus);\n"
  },
  {
    "path": "ui/src/containers/applications/MacOSEntApplicationPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {Container, Header} from \"semantic-ui-react\";\nimport {\n    post, PostActionRequest,\n  //  read, ReadActionRequest\n} from \"../../store/applications/actions\";\n// import {ApplicationForm, IFormData, IFormData as ApplicationFormData} from \"../../forms/ApplicationForm\";\nimport {RootState} from \"../../reducers\";\n\ninterface IReduxStateProps {\n\n}\n\ninterface IReduxDispatchProps {\n    post: PostActionRequest;\n}\n\ninterface IRouteParameters {\n    platform: string;\n    id?: string;\n}\n\nclass UnconnectedApplicationPage extends React.Component<IReduxStateProps & IReduxDispatchProps & RouteComponentProps<IRouteParameters>, void> {\n\n    public componentWillMount?() {\n        if (this.props.match.params.id) {\n            // this.props.read(this.props.match.params.id, ['applications']);\n        }\n    }\n\n    // public handleSubmit = (values: IFormData) => {\n    //     if (this.props.match.params.id) {\n    //         // this.props.patch()\n    //     } else {\n    //         this.props.post(values);\n    //     }\n    // };\n\n    public render() {\n        const { } = this.props;\n\n        return (\n            <Container>\n                <Header as=\"h1\">Application</Header>\n                {/*<ApplicationForm onSubmit={this.handleSubmit} onClickFetch={this.fetchManifestURL} />*/}\n            </Container>\n        );\n    }\n\n    public fetchManifestURL = (e: any) => {\n\n    }\n}\n\nexport const MacOSEntApplicationPage  = connect(\n    (state: RootState, ownProps?: any) => ({}),\n    (dispatch: Dispatch, ownProps?: any) => bindActionCreators({ post }, dispatch),\n)(UnconnectedApplicationPage);\n"
  },
  {
    "path": "ui/src/containers/config/DeviceAuthPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {Container, Header} from \"semantic-ui-react\";\nimport {DeviceAuthForm, IDeviceAuthFormValues} from \"../../components/forms/DeviceAuthForm\";\nimport {RootState} from \"../../reducers\";\nimport * as actions from \"../../store/configuration/scep_actions\";\nimport {SCEPState} from \"../../store/configuration/scep_reducer\";\n\ninterface ReduxStateProps {\n    scep: SCEPState;\n}\n\ninterface ReduxDispatchProps {\n    read: actions.ReadActionRequest;\n    post: actions.PostActionRequest;\n}\n\ninterface OwnProps extends ReduxStateProps, ReduxDispatchProps, RouteComponentProps<{}> {\n\n}\n\nexport class UnconnectedDeviceAuthPage extends React.Component<OwnProps, undefined> {\n\n    public componentWillMount?() {\n        this.props.read();\n    }\n\n    private handleSubmit = (values: IDeviceAuthFormValues): void => {\n        this.props.post({\n            ...values,\n            key_size: values.key_size,\n            key_usage: values.key_usage,\n        });\n    };\n\n    private handleTest = () => {\n\n    };\n\n    public render() {\n        const {\n            scep,\n        } = this.props;\n\n        const stringifiedData = {\n            ...scep.data,\n            key_size: scep.data ? \"\" + scep.data.key_size : null,\n            key_usage: scep.data ? \"\" + scep.data.key_usage : null,\n        };\n\n        return (\n            <Container className=\"SCEPPage\">\n                <Header as=\"h1\">Device Authentication</Header>\n                <p>\n                    Use this section to configure how your device will securely contact the MDM server.\n                </p>\n                <DeviceAuthForm loading={scep.loading} data={scep.data} onSubmit={this.handleSubmit} />\n            </Container>\n        )\n    }\n\n}\n\nexport const DeviceAuthPage = connect<ReduxStateProps, ReduxDispatchProps, OwnProps>(\n    (state: RootState): ReduxStateProps => ({\n        scep: state.configuration.scep,\n    }),\n    (dispatch: Dispatch) => bindActionCreators({\n        post: actions.post,\n        read: actions.read,\n    }, dispatch),\n)(UnconnectedDeviceAuthPage);\n"
  },
  {
    "path": "ui/src/containers/config/OrganizationPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {Link} from \"react-router-dom\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {IOrganizationFormValues, OrganizationForm} from \"../../components/forms/OrganizationForm\";\nimport {RSAAApiErrorMessage} from \"../../components/RSAAApiErrorMessage\";\nimport {RootState} from \"../../reducers/index\";\nimport * as actions from \"../../store/organization/actions\";\nimport {OrganizationState} from \"../../store/organization/reducer\";\n\nimport {\n    Breadcrumb,\n    Container,\n    Divider,\n    Header,\n} from \"semantic-ui-react\";\n\ninterface IOrganizationPageState {\n    organization: OrganizationState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): IOrganizationPageState {\n    return {\n        organization: state.organization,\n    }\n}\n\ninterface IOrganizationPageDispatchProps {\n    read: actions.ReadActionRequest;\n    post: actions.PostActionRequest;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch) {\n    return bindActionCreators({\n        post: actions.post,\n        read: actions.read,\n    }, dispatch);\n}\n\ninterface OrganizationPageProps extends IOrganizationPageState, IOrganizationPageDispatchProps, RouteComponentProps<any> {\n\n}\n\nexport class UnconnectedOrganizationPage extends React.Component<OrganizationPageProps, undefined> {\n\n    public componentWillMount?() {\n        this.props.read();\n    }\n\n    private handleSubmit: (values: IOrganizationFormValues) => void = (values) => {\n        this.props.post(values);\n    };\n\n    public render(): JSX.Element {\n        const {\n            organization,\n        } = this.props;\n\n        return (\n            <Container className=\"OrganizationPage\">\n                <Divider hidden />\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/settings`}>Settings</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active>Organization</Breadcrumb.Section>\n                </Breadcrumb>\n\n                <Header as=\"h1\">Organization</Header>\n                <p>Many parts of the system rely on showing your organization name in certain user facing scenarios.\n                    Configure these details here</p>\n                {organization.error && <RSAAApiErrorMessage error={organization.errorDetail} />}\n                {organization.organization &&\n                    <OrganizationForm\n                        loading={organization.loading}\n                        data={organization.organization}\n                        id={organization.organization.id}\n                        onSubmit={this.handleSubmit}\n                    />}\n\n            </Container>\n        )\n    }\n\n}\n\nexport const OrganizationPage = connect<IOrganizationPageState, IOrganizationPageDispatchProps, OrganizationPageProps>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(UnconnectedOrganizationPage);\n"
  },
  {
    "path": "ui/src/containers/devices/DeviceApplications.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps, RouteProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {DeviceApplicationsTable} from \"../../components/react-tables/DeviceApplicationsTable\";\nimport {RootState} from \"../../reducers/index\";\nimport {FlaskFilterOperation} from \"../../flask-rest-jsonapi\";\nimport {\n    applications as fetchInstalledApplications, InstalledApplicationsActionRequest,\n} from \"../../store/device/applications\";\nimport {InstalledApplicationsState} from \"../../store/device/installed_applications_reducer\";\nimport {IReactTableState} from \"../../store/table/types\";\nimport {FlaskFilter} from \"../../flask-rest-jsonapi\";\n\ninterface IReduxStateProps {\n    installed_applications?: InstalledApplicationsState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): IReduxStateProps {\n    return {\n        installed_applications: state.device.installed_applications,\n    }\n}\n\ninterface IReduxDispatchProps {\n    fetchInstalledApplications: InstalledApplicationsActionRequest\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch): IReduxDispatchProps {\n   return bindActionCreators({\n       fetchInstalledApplications,\n   }, dispatch);\n}\n\ninterface IDeviceApplicationsRouteProps {\n    id?: string;\n}\n\ntype DeviceApplicationsProps = IReduxStateProps &\n    IReduxDispatchProps &\n    RouteComponentProps<IDeviceApplicationsRouteProps>;\n\nexport class UnconnectedDeviceApplications extends React.Component<DeviceApplicationsProps, undefined> {\n    public render() {\n        const {\n            installed_applications,\n        } = this.props;\n\n        return (\n            <div className=\"DeviceApplications container\">\n                {installed_applications.items &&\n                <DeviceApplicationsTable\n                  data={installed_applications.items}\n                  defaultPageSize={installed_applications.pageSize}\n                  loading={installed_applications.loading}\n                  onFetchData={this.fetchData}\n                  pages={installed_applications.pages}\n                  defaultSorted={[\n                      { id: \"name\", desc: true },\n                  ]}\n                />}\n            </div>\n        )\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.fetchInstalledApplications(this.props.match.params.id, state.pageSize, state.page + 1, sorting, filtering);\n    }\n}\n\nexport const DeviceApplications = connect<IReduxStateProps, IReduxDispatchProps, any>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(UnconnectedDeviceApplications);\n"
  },
  {
    "path": "ui/src/containers/devices/DeviceCertificates.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {certificates as fetchInstalledCertificates, CertificatesActionRequest} from \"../../store/device/certificates\";\nimport {InstalledCertificatesState} from \"../../store/device/installed_certificates_reducer\";\nimport {RootState} from \"../../reducers/index\";\nimport {DeviceCertificatesTable} from \"../../components/react-tables/DeviceCertificatesTable\";\nimport {IReactTableState} from \"../../store/table/types\";\nimport {FlaskFilterOperation} from \"../../flask-rest-jsonapi\";\nimport {FlaskFilter} from \"../../flask-rest-jsonapi\";\n\ninterface IReduxStateProps {\n    installed_certificates: InstalledCertificatesState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): IReduxStateProps {\n    return {\n        installed_certificates: state.device.installed_certificates,\n    };\n}\n\ninterface IReduxDispatchProps {\n    fetchInstalledCertificates: CertificatesActionRequest;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch): IReduxDispatchProps {\n    return bindActionCreators({\n        fetchInstalledCertificates,\n    }, dispatch);\n}\n\ninterface IDeviceCertificatesRouteProps {\n    id?: string;\n}\n\ntype DeviceCertificatesProps = IReduxStateProps &\n    IReduxDispatchProps &\n    RouteComponentProps<IDeviceCertificatesRouteProps>;\n\nexport class UnconnectedDeviceCertificates extends React.Component<DeviceCertificatesProps, any> {\n    public render(): JSX.Element {\n        const {\n            installed_certificates,\n        } = this.props;\n\n        return (\n            <div className=\"DeviceCertificates\">\n                {installed_certificates.items &&\n                <DeviceCertificatesTable\n                    data={installed_certificates.items}\n                    defaultPageSize={installed_certificates.pageSize}\n                    loading={installed_certificates.loading}\n                    onFetchData={this.fetchData}\n                    pages={installed_certificates.pages}\n                />}\n            </div>\n        );\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.fetchInstalledCertificates(this.props.match.params.id, state.pageSize, state.page + 1, sorting, filtering);\n    }\n}\n\nexport const DeviceCertificates = connect<IReduxStateProps, IReduxDispatchProps, DeviceCertificatesProps>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(UnconnectedDeviceCertificates);\n"
  },
  {
    "path": "ui/src/containers/devices/DeviceCommands.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps, RouteProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {DeviceCommandsTable} from \"../../components/react-tables/DeviceCommandsTable\";\nimport {RootState} from \"../../reducers/index\";\nimport {FlaskFilterOperation} from \"../../flask-rest-jsonapi\";\nimport {commands as fetchCommands, CommandsActionRequest} from \"../../store/device/actions\";\nimport {DeviceCommandsState} from \"../../store/device/commands_reducer\";\nimport {IReactTableState} from \"../../store/table/types\";\nimport {FlaskFilter} from \"../../flask-rest-jsonapi\";\n\ninterface IReduxStateProps {\n    commands?: DeviceCommandsState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): IReduxStateProps {\n    return {\n        commands: state.device.commands,\n    }\n}\n\ninterface IReduxDispatchProps {\n    fetchCommands: CommandsActionRequest\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch): IReduxDispatchProps {\n   return bindActionCreators({\n       fetchCommands,\n   }, dispatch);\n}\n\ninterface IDeviceCommandsRouteProps {\n    id?: string;\n}\n\ntype DeviceCommandsProps = IReduxStateProps & IReduxDispatchProps & RouteComponentProps<IDeviceCommandsRouteProps>;\n\nexport class UnconnectedDeviceCommands extends React.Component<DeviceCommandsProps, any> {\n\n    // componentWillMount?() {\n    //     this.props.fetchCommands(''+this.props.match.params.id, 10, 1, ['-sent_at']);\n    // }\n\n    public render() {\n        const {\n            commands,\n        } = this.props;\n\n        return (\n            <div className=\"DeviceCommands container\">\n                <DeviceCommandsTable\n                    data={commands.items}\n                    defaultPageSize={commands.pageSize}\n                    loading={commands.loading}\n                    onFetchData={this.fetchData}\n                    pages={commands.pages}\n                    defaultSorted={[\n                        { id: \"sent_at\", asc: true },\n                    ]}\n                />\n            </div>\n        )\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.fetchCommands(this.props.match.params.id, state.pageSize, state.page + 1, sorting, filtering);\n    }\n}\n\nexport const DeviceCommands = connect<IReduxStateProps, IReduxDispatchProps, any>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(UnconnectedDeviceCommands);\n"
  },
  {
    "path": "ui/src/containers/devices/DeviceDetail.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {DeviceState} from \"../../store/device/reducer\";\nimport {RootState} from \"../../reducers/index\";\n\nimport {Grid, Header, List} from \"semantic-ui-react\";\n\nimport {CheckListItem} from \"../../components/CheckListItem\";\n\ninterface ReduxStateProps {\n    device: DeviceState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): ReduxStateProps {\n    return {\n        device: state.device,\n    };\n}\n\ninterface ReduxDispatchProps {\n\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch<any>): ReduxDispatchProps {\n    return bindActionCreators({\n\n    }, dispatch);\n}\n\ninterface DeviceCommandsRouteProps {\n\n}\n\ninterface DeviceDetailProps extends ReduxStateProps, ReduxDispatchProps, RouteComponentProps<DeviceCommandsRouteProps> {\n\n}\n\ninterface DeviceDetailComponentState {\n\n}\n\nclass UnconnectedDeviceDetail extends React.Component<DeviceDetailProps, DeviceDetailComponentState> {\n    render() {\n        const {\n            device: {device},\n        } = this.props;\n\n        return (\n            <Grid columns={2} className=\"DeviceDetail\">\n                <Grid.Row>\n                    <Grid.Column>\n                        <Header>Security</Header>\n                        {device &&\n                        <List>\n                            <CheckListItem title=\"Firewall Enabled\" value={device.attributes.firewall_enabled}>\n                                <CheckListItem title=\"Stealth Mode\" value={device.attributes.stealth_mode_enabled}/>\n                                <CheckListItem title=\"Block all incoming\" value={device.attributes.block_all_incoming}/>\n                            </CheckListItem>\n                            <CheckListItem title=\"Has Passcode\" value={device.attributes.passcode_present}>\n                                <CheckListItem title=\"Passcode is compliant\" value={device.attributes.passcode_compliant}/>\n                                <CheckListItem title=\"Passcode is compliant with profiles\" value={device.attributes.passcode_compliant_with_profiles}/>\n                            </CheckListItem>\n                            <CheckListItem title=\"Full Disk Encryption Enabled\" value={device.attributes.fde_enabled}>\n                                <CheckListItem title=\"With Personal Recovery Key\" value={device.attributes.fde_has_prk}/>\n                                <CheckListItem title=\"With Institutional Recovery Key\" value={device.attributes.fde_has_irk}/>\n                            </CheckListItem>\n                        </List>}\n                    </Grid.Column>\n                    <Grid.Column>\n                        <Header>iTunes and iCloud</Header>\n                        {device &&\n                        <List>\n                            <CheckListItem title=\"Store account active\" value={device.attributes.itunes_store_account_is_active}/>\n                            <CheckListItem title=\"iCloud Backup Enabled\" value={device.attributes.is_cloud_backup_enabled}>\n                                {device.attributes.is_cloud_backup_enabled &&\n                                <CheckListItem title={\"Last backup date\"}\n                                               value={device.attributes.last_cloud_backup_date}/>\n                                }\n                            </CheckListItem>\n                            <CheckListItem title=\"Find my iPhone enabled\" value={device.attributes.is_device_locator_service_enabled} />\n                        </List>\n                        }\n                    </Grid.Column>\n\n                </Grid.Row>\n            </Grid>\n        );\n    }\n}\n\nexport const DeviceDetail = connect<ReduxStateProps, ReduxDispatchProps, any>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(UnconnectedDeviceDetail);\n"
  },
  {
    "path": "ui/src/containers/devices/DeviceOSUpdates.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps, RouteProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {FlaskFilter, FlaskFilters} from \"../../flask-rest-jsonapi\";\nimport {DeviceUpdatesTable} from \"../../components/react-tables/DeviceUpdatesTable\";\nimport {RootState} from \"../../reducers\";\nimport {AvailableOSUpdatesState} from \"../../store/device/available_os_updates_reducer\";\nimport {AvailableOSUpdatesActionRequest, updates as fetchAvailableOSUpdates} from \"../../store/device/updates\";\n\nimport {\n    Button,\n    Divider,\n    Checkbox,\n} from \"semantic-ui-react\";\n\nimport {IReactTableState} from \"../../store/table/types\";\nimport {FlaskFilterOperation} from \"../../flask-rest-jsonapi\";\n\ninterface IReduxStateProps {\n    is_supervised?: boolean;\n    updates?: AvailableOSUpdatesState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): IReduxStateProps {\n    return {\n        is_supervised: state.device.device.attributes.is_supervised,\n        updates: state.device.available_os_updates,\n    };\n}\n\ninterface IReduxDispatchProps {\n    fetchAvailableOSUpdates: AvailableOSUpdatesActionRequest;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch): IReduxDispatchProps {\n   return bindActionCreators({\n       fetchAvailableOSUpdates,\n   }, dispatch);\n}\n\ninterface IDeviceOSUpdatesRouteProps {\n    id?: string;\n}\n\ninterface IDeviceOSUpdatesProps extends IReduxStateProps, IReduxDispatchProps,\n    RouteComponentProps<IDeviceOSUpdatesRouteProps> {\n\n}\n\ninterface IBaseDeviceOSUpdatesState {\n    hide_config_updates: boolean;\n}\n\nclass BaseDeviceOSUpdates extends React.Component<IDeviceOSUpdatesProps, IBaseDeviceOSUpdatesState> {\n\n    public state: IBaseDeviceOSUpdatesState = {\n      hide_config_updates: true,\n    };\n\n    // public componentWillMount?() {\n    //     const filters: FlaskFilters = [\n    //         { name: \"is_config_data_update\", op: \"ne\", val: \"1\" },\n    //     ];\n    //\n    //     this.props.fetchAvailableOSUpdates(\"\" + this.props.match.params.id,\n    //         this.props.griddleState.pageSize, 1, [], filters);\n    // }\n\n    public render() {\n        const {\n            updates,\n            is_supervised,\n        } = this.props;\n\n        return (\n            <div className=\"DeviceOSUpdates container\">\n                <Checkbox label=\"Hide configuration data updates (XProtect, Gatekeeper)\"\n                          checked={this.state.hide_config_updates}\n                          onChange={(e) => this.setState({ hide_config_updates: !this.state.hide_config_updates })}\n                />\n                {is_supervised ?\n                    <Button size=\"small\" floated=\"right\">Update All</Button> :\n                    <Button size=\"small\" title=\"Unsupervised\" floated=\"right\" disabled>Update All</Button> }\n                <Divider/>\n                <DeviceUpdatesTable\n                    data={updates.items}\n                    defaultPageSize={updates.pageSize}\n                    loading={updates.loading}\n                    onFetchData={this.fetchData}\n                    pages={updates.pages}\n                />\n            </div>\n        );\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.fetchAvailableOSUpdates(this.props.match.params.id, state.pageSize, state.page + 1, sorting, filtering);\n    }\n}\n\nexport const DeviceOSUpdates = connect<IReduxStateProps, IReduxDispatchProps, any>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(BaseDeviceOSUpdates);\n"
  },
  {
    "path": "ui/src/containers/devices/DeviceProfiles.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps, RouteProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {DeviceProfilesTable} from \"../../components/react-tables/DeviceProfilesTable\";\nimport {InstalledProfilesState} from \"../../store/device/installed_profiles_reducer\";\nimport {RootState} from \"../../reducers/index\";\nimport {InstalledProfilesActionRequest, profiles as fetchInstalledProfiles} from \"../../store/device/profiles\";\nimport {IReactTableState} from \"../../store/table/types\";\nimport {FlaskFilterOperation} from \"../../flask-rest-jsonapi\";\nimport {FlaskFilter} from \"../../flask-rest-jsonapi\";\n\ninterface ReduxStateProps {\n    profiles?: InstalledProfilesState;\n}\n\nfunction mapStateToProps(state: RootState, ownProps?: any): ReduxStateProps {\n    return {\n        profiles: state.device.installed_profiles,\n    }\n}\n\ninterface ReduxDispatchProps {\n    fetchInstalledProfiles: InstalledProfilesActionRequest;\n}\n\nfunction mapDispatchToProps(dispatch: Dispatch): ReduxDispatchProps {\n   return bindActionCreators({\n       fetchInstalledProfiles,\n   }, dispatch);\n}\n\ninterface DeviceProfilesRouteProps {\n    id?: string;\n}\n\ninterface DeviceProfilesProps extends ReduxStateProps, ReduxDispatchProps, RouteComponentProps<DeviceProfilesRouteProps> {\n\n}\n\nclass UnconnectedDeviceProfiles extends React.Component<DeviceProfilesProps, undefined> {\n\n    // componentWillMount?() {\n    //     this.props.fetchInstalledProfiles(''+this.props.match.params.id, this.props.griddleState.pageSize, 1);\n    // }\n\n    public render() {\n        const {\n            profiles,\n        } = this.props;\n\n        return (\n            <div className=\"DeviceProfiles container\">\n                <DeviceProfilesTable\n                    data={profiles.items}\n                    defaultPageSize={profiles.pageSize}\n                    loading={profiles.loading}\n                    onFetchData={this.fetchData}\n                    pages={profiles.pages}\n                />\n            </div>\n        )\n    }\n\n    private fetchData = (state: IReactTableState) => {\n        const sorting = state.sorted.map((value) => (value.desc ? value.id : \"-\" + value.id));\n        const filtering: FlaskFilter[] = state.filtered.map((value) => {\n            return {\n                name: value.id,\n                op: \"ilike\" as FlaskFilterOperation,\n                val: `%25${value.value}%25`,\n            };\n        });\n\n        this.props.fetchInstalledProfiles(this.props.match.params.id, state.pageSize, state.page + 1, sorting, filtering);\n    }\n}\n\nexport const DeviceProfiles = connect<ReduxStateProps, ReduxDispatchProps, any>(\n    mapStateToProps,\n    mapDispatchToProps,\n)(UnconnectedDeviceProfiles);\n"
  },
  {
    "path": "ui/src/containers/settings/APNSPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect} from \"react-redux\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators, Dispatch} from \"redux\";\nimport {RootState} from \"../../reducers\";\n\nimport Dropzone, {DropFilesEventHandler} from \"react-dropzone\";\nimport {Link} from \"react-router-dom\";\n\nimport {Component, SyntheticEvent} from \"react\";\nimport {RSAAApiErrorMessage} from \"../../components/RSAAApiErrorMessage\";\nimport {APNSState} from \"../../store/configuration/apns_reducer\";\nimport {\n    csr,\n    CsrActionRequest,\n    uploadCrypted,\n    UploadCryptedActionRequest,\n} from \"../../store/configuration/mdmcert_actions\";\n\nimport {Breadcrumb, Button, Container, Divider, Header, Icon, Input, Message, Segment} from \"semantic-ui-react\";\n\ninterface IReduxStateProps {\n    apns: APNSState;\n}\n\ninterface IReduxDispatchProps {\n    csr: CsrActionRequest;\n    uploadCrypted: UploadCryptedActionRequest;\n}\n\nexport type APNSPageProps = IReduxStateProps & IReduxDispatchProps & RouteComponentProps<any>;\n\ninterface IAPNSPageState {\n    email: string;\n}\n\nexport class UnconnectedAPNSPage extends Component<APNSPageProps, IAPNSPageState> {\n\n    public state: IAPNSPageState = {email: \"\"};\n\n    public render() {\n        const {apns} = this.props;\n        const mdmcertSuccess = apns && apns.csrResult && apns.csrResult.result === \"success\";\n\n        return (\n            <Container className=\"APNSPage\">\n                <Divider hidden/>\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider/>\n                    <Breadcrumb.Section><Link to={`/settings`}>Settings</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider/>\n                    <Breadcrumb.Section active>Push Certificate</Breadcrumb.Section>\n                </Breadcrumb>\n\n                <Header as=\"h1\">Push Certificate</Header>\n                <p>\n                    A push certificate is required to tell devices to check in.\n                    We use the site <strong>mdmcert.download</strong> to issue a Push Certificate.\n                </p>\n                <Segment vertical>\n                    <Header>\n                        <Icon name=\"signup\" />\n                        <Header.Content>\n                            1. Register with mdmcert.download\n                        </Header.Content>\n                    </Header>\n\n                    <a href=\"https://mdmcert.download/registration\" target=\"_new\">Register</a> a new e-mail address\n                    with <strong>mdmcert.download</strong>. This e-mail will be used to send your signed certificate\n                    request.\n                </Segment>\n                <Segment vertical>\n                    <Header>\n                        <Icon name=\"send\" />\n                        <Header.Content>\n                            2. Get a certificate request signed and sent to this E-mail address\n                        </Header.Content>\n                    </Header>\n\n                    <Input iconPosition=\"left\" placeholder=\"Step 1 Registered E-mail\" fluid value={this.state.email}\n                           onChange={this.handleEmailChange} error={!this.state.email}\n                           loading={apns && apns.csrLoading}>\n                        <Icon name=\"mail\"/>\n                        <input/>\n                    </Input>\n                    <br/>\n                    <Button primary onClick={this.handleSendMdmcertCsr}\n                            disabled={apns && apns.csrLoading}>{mdmcertSuccess ? \"Sent\" : \"Send\"}</Button> a new encrypted certificate signing request\n                    (.csr) to the e-mail\n                    registered in step 1.\n\n                    {apns && apns.csrResult && apns.csrResult.result === \"failure\" &&\n                    <Message error>{apns.csrResult.reason}</Message>}\n                    {apns && apns.csrResult && apns.csrResult.result === \"success\" &&\n                    <Message success>OK, e-mail sent to the address above.</Message>}\n                </Segment>\n                <Segment vertical>\n                    <Header>\n                        <Icon name=\"upload\" />\n                        <Header.Content>\n                            3. Save the attachment from the e-mail (.p7) and upload here\n                        </Header.Content>\n                    </Header>\n\n                    <Dropzone onDrop={this.onDropEncryptedCSR}>\n                        {({getRootProps, getInputProps, isDragActive}) => {\n                            return (\n                                <div\n                                    {...getRootProps()}\n                                    className={\"dropzone\"}\n                                >\n                                    <input {...getInputProps()} />\n                                    {\n                                        isDragActive ?\n                                            <p>Drop files here...</p> :\n                                            <p>Try dropping some files here, or click to select files to upload.<br />\n                                                Expecting <strong>mdm_signed_request.(timestamp).plist.b64.p7</strong></p>\n                                    }\n                                </div>\n                            )\n\n                        }}\n                    </Dropzone>\n                    {apns && apns.decryptError && <RSAAApiErrorMessage error={apns.decryptError}/>}\n                </Segment>\n                <Segment vertical>\n                    <Header>\n                        <Icon name=\"download\" />\n                        <Header.Content>\n                            4. Download\n                        </Header.Content>\n                    </Header>\n\n                    <Button icon labelPosition=\"left\">\n                        <Icon name=\"download\"/> Download\n                    </Button> the decrypted Certificate Signing Request\n                </Segment>\n                <Segment vertical>\n                    <Header>5. Upload to Push Portal</Header>\n\n                    Upload the .csr to the <a href=\"https://identity.apple.com/pushcert/\" target=\"_new\">Apple Push\n                    Portal</a>\n                </Segment>\n                <Segment vertical>\n                    <Header>6. Download the push certificate from the Apple Push Portal</Header>\n                </Segment>\n\n            </Container>\n        )\n    }\n\n    private handleSendMdmcertCsr = (e: any) => {\n        if (this.state.email) {\n            this.props.csr(this.state.email);\n        }\n    };\n\n    private handleEmailChange = (e: SyntheticEvent<HTMLInputElement>) => {\n        this.setState({email: e.currentTarget.value});\n    };\n\n    private onDropEncryptedCSR: DropFilesEventHandler = (accepted, rejected) => {\n        console.dir(accepted);\n        for (const file of accepted) {\n            this.props.uploadCrypted(file);\n        }\n    };\n}\n\nexport const APNSPage = connect<IReduxStateProps, IReduxDispatchProps, RouteComponentProps<any>>(\n    (state: RootState): IReduxStateProps => ({\n        apns: state.configuration.apns,\n    }),\n    (dispatch: Dispatch) => bindActionCreators({\n        csr,\n        uploadCrypted,\n    }, dispatch),\n)(UnconnectedAPNSPage);\n"
  },
  {
    "path": "ui/src/containers/settings/DEPAccountSetupPage.tsx",
    "content": "import {IDEPAccountsState} from \"../../store/dep/accounts_reducer\";\nimport {accounts, AccountIndexActionCreator} from \"../../store/dep/actions\";\nimport {RouteComponentProps} from \"react-router\";\nimport * as React from \"react\";\nimport {\n    Container,\n    Header,\n    Icon,\n    Button,\n    Step,\n    Grid,\n    Divider,\n} from \"semantic-ui-react\";\n\nimport {connect} from \"react-redux\";\nimport {RootState} from \"../../reducers\";\nimport {bindActionCreators, Dispatch} from \"redux\";\n\ninterface ReduxStateProps {\n    accounts: IDEPAccountsState;\n}\n\ninterface ReduxDispatchProps {\n    getAccounts: AccountIndexActionCreator;\n}\n\ninterface OwnProps extends ReduxStateProps, ReduxDispatchProps, RouteComponentProps<any> {\n\n}\n\ninterface IDEPAccountSetupPageState {\n    step: number;\n}\n\nexport class UnconnectedDEPAccountSetupPage extends React.Component<OwnProps, IDEPAccountSetupPageState> {\n\n    // static initialState: IDEPAccountPageState = {\n    //     step: 0\n    // };\n\n    constructor(props: any) {\n        super(props);\n        this.state = { step: 0 };\n    }\n\n    public componentWillMount?() {\n        //this.props.index();\n    }\n\n    public render() {\n        const {\n            accounts: {data, loading},\n        } = this.props;\n\n        return (\n            <Container className=\"DEPAccountPage\">\n                <Header as=\"h1\">Set up a New DEP Account</Header>\n                <p>\n                    Set up a New DEP Account to Sync Devices from Apple Business Manager or Apple School Manager to start\n                    syncing your devices.\n                </p>\n                <Grid columns={2}>\n                    <Grid.Column width={6}>\n                        <Step.Group fluid vertical>\n                            <Step active={this.state.step == 0} onClick={() => this.setState({ step: 0 })}>\n                                <Icon name=\"download\" />\n                                <Step.Content>\n                                    <Step.Title>Download</Step.Title>\n                                    <Step.Description>Download a Public Key</Step.Description>\n                                </Step.Content>\n                            </Step>\n                            <Step active={this.state.step == 1} onClick={() => this.setState({ step: 1 })}>\n                                <Icon name=\"key\" />\n                                <Step.Content>\n                                    <Step.Title>Upload Key</Step.Title>\n                                    <Step.Description>Upload it to ABM/ASM to Get a Server Token</Step.Description>\n                                </Step.Content>\n                            </Step>\n                            <Step active={this.state.step == 2} onClick={() => this.setState({ step: 2 })}>\n                                <Icon name=\"upload\" />\n                                <Step.Content>\n                                    <Step.Title>Upload Token</Step.Title>\n                                    <Step.Description>Upload Server Token</Step.Description>\n                                </Step.Content>\n                            </Step>\n                            <Step active={this.state.step == 3} onClick={() => this.setState({ step: 3 })}>\n                                <Icon name=\"flag checkered\" />\n                                <Step.Content>\n                                    <Step.Title>Sync Devices</Step.Title>\n                                    <Step.Description>Start Syncing Devices</Step.Description>\n                                </Step.Content>\n                            </Step>\n                        </Step.Group>\n                    </Grid.Column>\n                    <Grid.Column width={10}>\n                        {this.state.step == 0 && <div>\n                        <Button icon labelPosition='left' as={'a'} href=\"/dep/certificate/download\" >\n                            <Icon name='download' />\n                            Download\n                        </Button> a Public Key to use with Apple Business Manager or Apple School Manager\n                        <Divider />\n                        <p>\n                            Before you can use the Public Key, you must create a new <em>MDM Server</em> in\n                            Apple School Manager or Apple Business Manager.\n                        </p>\n\n                        <p>\n                            Selecting the MDM Server you just created gives you the option to upload the Public Key\n                            generated here.\n                        </p>\n                        </div>}\n\n                        {this.state.step == 1 && <div>\n                            <ul>\n                                <li>Locate the Public Key <strong>(.cer file)</strong> downloaded in Step 1.</li>\n                                <li>Log in to Apple School Manager or Apple Business Manager.</li>\n                                <li>Create an MDM Server, or select one that you have already created.</li>\n                                <li>Click the <Icon name=\"key\" />Upload Key Button</li>\n                            </ul>\n                        </div>}\n\n                    </Grid.Column>\n                </Grid>\n            </Container>\n        );\n    }\n}\n\nexport const DEPAccountSetupPage = connect<ReduxStateProps, ReduxDispatchProps, OwnProps>(\n    (state: RootState): ReduxStateProps => ({\n        accounts: state.dep.accounts,\n    }),\n    (dispatch: Dispatch) => bindActionCreators({\n        getAccounts: accounts,\n    }, dispatch),\n)(UnconnectedDEPAccountSetupPage);\n"
  },
  {
    "path": "ui/src/containers/settings/DEPAccountsPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect, MapStateToProps} from \"react-redux\";\nimport {Dispatch} from \"redux\";\n\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators} from \"redux\";\nimport {IDEPAccountsState} from \"../../store/dep/accounts_reducer\";\nimport {RootState} from \"../../reducers/index\";\nimport {DEPAccountsTable} from \"../../components/react-tables/DEPAccountsTable\";\nimport {Link} from \"react-router-dom\";\nimport {\n    AccountIndexActionCreator, accounts,\n} from \"../../store/dep/actions\";\n\n\nimport {\n    Container,\n    Header,\n    Grid,\n    Icon,\n    Button,\n    Divider,\n    Breadcrumb,\n} from \"semantic-ui-react\";\n\ninterface RouteProps {\n\n}\n\ninterface ReduxStateProps {\n    accounts: IDEPAccountsState;\n}\n\ninterface ReduxDispatchProps {\n    getAccounts: AccountIndexActionCreator;\n}\n\ninterface OwnProps extends ReduxStateProps, ReduxDispatchProps, RouteComponentProps<RouteProps> {\n\n}\n\nexport class UnconnectedDEPAccountsPage extends React.Component<OwnProps, void> {\n\n    public componentWillMount?() {\n        this.props.getAccounts();\n    }\n\n\n    public render() {\n        const {\n            accounts,\n        } = this.props;\n\n        return (\n            <Container className=\"DEPAccountsPage\">\n                <Divider hidden />\n                <Breadcrumb>\n                    <Breadcrumb.Section><Link to={`/`}>Home</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section><Link to={`/settings`}>Settings</Link></Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active>DEP Accounts</Breadcrumb.Section>\n                </Breadcrumb>\n\n                <Header as=\"h1\">DEP Accounts</Header>\n                <Grid>\n                    <Grid.Column>\n                        <Button icon labelPosition='left' as={Link} to=\"/settings/dep/accounts/add\">\n                            <Icon name='plus' />\n                            New\n                        </Button>\n                    </Grid.Column>\n                </Grid>\n                <Grid>\n                    <Grid.Column>\n                        <DEPAccountsTable data={accounts.data}\n                                          loading={accounts.loading} />\n                    </Grid.Column>\n                </Grid>\n            </Container>\n        );\n    }\n}\n\nexport const DEPAccountsPage = connect<ReduxStateProps, ReduxDispatchProps>(\n    (state: RootState): ReduxStateProps => ({\n        accounts: state.dep.accounts,\n    }),\n    (dispatch: Dispatch) => bindActionCreators({\n        getAccounts: accounts,\n    }, dispatch),\n)(UnconnectedDEPAccountsPage);\n"
  },
  {
    "path": "ui/src/containers/settings/VPPAccountsPage.tsx",
    "content": "import * as React from \"react\";\nimport {connect, MapStateToProps} from \"react-redux\";\nimport {Dispatch} from \"redux\";\nimport {\n    Container,\n    Header,\n} from \"semantic-ui-react\";\n\nimport {Component} from \"react\";\nimport * as Dropzone from \"react-dropzone\";\nimport {RouteComponentProps} from \"react-router\";\nimport {bindActionCreators} from \"redux\";\nimport {RootState} from \"../../reducers/index\";\nimport {index, IndexActionRequest,\n    read as fetchTokenInfo, TokenActionRequest} from \"../../store/configuration/vpp\";\nimport {VPPState} from \"../../store/configuration/vpp_reducer\";\n\ninterface IReduxStateProps {\n    vpp: VPPState;\n}\n\ninterface IReduxDispatchProps {\n    fetchTokenInfo: TokenActionRequest;\n    index: IndexActionRequest;\n}\n\nexport type UnconnectedVPPAccountsPageProps = IReduxStateProps & IReduxDispatchProps & RouteComponentProps<any>\n\nexport class UnconnectedVPPAccountsPage extends Component<UnconnectedVPPAccountsPageProps, any> {\n\n    public componentWillMount?() {\n        this.props.index();\n    }\n\n    // handleDrop = (files: File[]) => {\n    //     this.props.upload(files[0]);\n    // };\n\n    public render() {\n        const {\n            vpp: {data, loading},\n        } = this.props;\n\n        return (\n            <Container className=\"VPPAccountsPage\">\n                <Header as=\"h1\">VPP Accounts</Header>\n                {/*<Dropzone*/}\n                    {/*onDrop={this.handleDrop}*/}\n                    {/*className=\"dropzone\"*/}\n                    {/*activeClassName=\"dropzone-active\"*/}\n                    {/*rejectClassName=\"dropzone-reject\"*/}\n                    {/*style={{}}>*/}\n                    {/*<Header as=\"h3\">Drop .vpptoken or Click to upload</Header>*/}\n                {/*</Dropzone>*/}\n            </Container>\n        );\n    }\n}\n\nexport const VPPAccountsPage = connect(\n    (state: RootState): IReduxStateProps => ({\n        vpp: state.configuration.vpp,\n    }),\n    (dispatch: Dispatch) => bindActionCreators({\n        fetchTokenInfo,\n        index,\n    }, dispatch),\n)(UnconnectedVPPAccountsPage);\n"
  },
  {
    "path": "ui/src/entry.tsx",
    "content": "import createHistory from \"history/createBrowserHistory\";\nimport * as React from \"react\";\nimport {render} from \"react-dom\";\nimport {Provider} from \"react-redux\";\nimport {Route} from \"react-router\";\n\nimport {RootState} from \"./reducers\";\nimport {configureStore, history} from \"./store/configureStore\";\n\nimport {App} from \"./components/App\";\n\nimport {ApplicationPage} from \"./containers/ApplicationPage\";\nimport {MacOSEntApplicationPage} from \"./containers/applications/MacOSEntApplicationPage\";\nimport {ApplicationsPage} from \"./containers/ApplicationsPage\";\nimport {AppStorePage} from \"./containers/AppStorePage\";\nimport {DeviceAuthPage} from \"./containers/config/DeviceAuthPage\";\nimport {OrganizationPage} from \"./containers/config/OrganizationPage\";\nimport {DashboardPage} from \"./containers/DashboardPage\";\nimport {DEPAccountPage} from \"./containers/DEPAccountPage\";\nimport {DEPProfilePage} from \"./containers/DEPProfilePage\";\nimport {DevicePage} from \"./containers/DevicePage\";\nimport {DevicesPage} from \"./containers/DevicesPage\";\nimport {ProfilePage} from \"./containers/ProfilePage\";\nimport {ProfilesPage} from \"./containers/ProfilesPage\";\nimport {APNSPage} from \"./containers/settings/APNSPage\";\nimport {DEPAccountSetupPage} from \"./containers/settings/DEPAccountSetupPage\";\nimport {DEPAccountsPage} from \"./containers/settings/DEPAccountsPage\";\nimport {VPPAccountsPage} from \"./containers/settings/VPPAccountsPage\";\nimport {SettingsPage} from \"./containers/SettingsPage\";\nimport {LoginPage} from \"./containers/LoginPage\";\nimport {LogoutPage} from \"./containers/LogoutPage\";\n\nimport \"../sass/app.scss\";\nimport {ProfileUpload} from \"./containers/ProfileUpload\";\n\nconst initialState: RootState = {};\n\nimport { ConnectedRouter, routerMiddleware } from \"connected-react-router\";\nimport {NavigationLayout} from \"./components/NavigationLayout\";\nimport {BareLayout} from \"./components/BareLayout\";\nimport {ProtectedRoute} from \"./components/ProtectedRoute\";\n\nconst store = configureStore(initialState, routerMiddleware(history));\n\nrender(\n    <Provider store={store}>\n        <ConnectedRouter history={history}>\n            <BareLayout exact path=\"/login\" component={LoginPage} />\n            <BareLayout exact path=\"/logout\" component={LogoutPage} />\n            <App>\n                <NavigationLayout>\n                    <ProtectedRoute exact path=\"/\" component={DashboardPage} />\n                    <ProtectedRoute exact path=\"/applications\" component={ApplicationsPage} />\n                    <ProtectedRoute path=\"/applications/id/:id\" component={ApplicationPage} />\n                    <ProtectedRoute path=\"/applications/add/macos\" component={MacOSEntApplicationPage} />\n                    <ProtectedRoute path=\"/applications/add/:entity\" component={AppStorePage} />\n                    <ProtectedRoute exact path=\"/devices\" component={DevicesPage} />\n                    <ProtectedRoute path=\"/devices/:id\" component={DevicePage} />\n                    <ProtectedRoute exact path=\"/profiles\" component={ProfilesPage} />\n                    <ProtectedRoute path=\"/profiles/add/custom\" component={ProfileUpload} />\n                    <ProtectedRoute path=\"/profiles/id/:id\" component={ProfilePage} />\n                    <ProtectedRoute exact path=\"/settings\" component={SettingsPage} />\n                    <ProtectedRoute path=\"/settings/apns\" component={APNSPage} />\n                    <ProtectedRoute path=\"/settings/deviceauth\" component={DeviceAuthPage} />\n                    <ProtectedRoute path=\"/settings/organization\" component={OrganizationPage} />\n                    <ProtectedRoute path=\"/settings/vpp\" component={VPPAccountsPage} />\n                    <ProtectedRoute exact path=\"/settings/dep/accounts\" component={DEPAccountsPage} />\n                    <ProtectedRoute path=\"/settings/dep/accounts/add\" component={DEPAccountSetupPage} />\n                    <ProtectedRoute exact path=\"/dep/accounts/:id\" component={DEPAccountPage} />\n                    <ProtectedRoute exact path=\"/dep/accounts/:account_id/add/profile\" component={DEPProfilePage} />\n                    <ProtectedRoute exact path=\"/dep/accounts/:account_id/profiles/:id\" component={DEPProfilePage} />\n                </NavigationLayout>\n            </App>\n        </ConnectedRouter>\n    </Provider>,\n    document.getElementById(\"root\") as HTMLElement,\n);\n"
  },
  {
    "path": "ui/src/flask-rest-jsonapi.ts",
    "content": "type WrappedChildIndexActionCreator<R> = (id: string, queryParameters: string[]) => R;\ntype WrappedIndexActionCreator<R> = (queryParameters: string[]) => R;\n\nexport type FlaskFilterOperation = \"any\" | \"between\" | \"endswith\" | \"eq\" | \"ge\" | \"gt\" |\n    \"has\" | \"ilike\" | \"in_\" | \"is_\" | \"isnot\" | \"like\" | \"le\" | \"lt\" | \"match\" | \"ne\" | \"notlike\" |\n    \"notin_\" | \"notlike\" | \"startswith\";\n\nexport interface FlaskFilter {\n    name: string;\n    op: FlaskFilterOperation;\n    val?: string;\n    field?: string;\n}\n\nexport type FlaskFilters = FlaskFilter[];\n\n/**\n * This higher order function processes the standard JSON-API index action creator and provides the already encoded\n * URL query to be appended to the JSON-API endpoint URL.\n *\n * @param wrappedActionCreator\n */\nexport const encodeJSONAPIIndexParameters = <R>(wrappedActionCreator: WrappedIndexActionCreator<R>) => (\n    size: number = 10,\n    pageNumber: number = 1,\n    sort?: string[],\n    filters?: FlaskFilters,\n    include?: string[],\n) => {\n    const queryParameters = [];\n\n    queryParameters.push(`page[size]=${size}`);\n    queryParameters.push(`page[number]=${pageNumber}`);\n\n    if (sort && sort.length > 0) {\n        queryParameters.push(\"sort=\" + sort.join(\",\"));\n    }\n\n    if (filters && filters.length > 0) {\n        queryParameters.push(\"filter=\" + JSON.stringify(filters));\n    }\n\n    if (include && include.length > 0) {\n        queryParameters.push(\"include=\" + include.join(\",\"));\n    }\n\n    return wrappedActionCreator(queryParameters);\n};\n/**\n * This higher order function processes the standard JSON-API index action creator and provides the already encoded\n * URL query to be appended to the JSON-API endpoint URL.\n *\n * @param wrappedActionCreator\n */\nexport const encodeJSONAPIChildIndexParameters = <R>(wrappedActionCreator: WrappedChildIndexActionCreator<R>) => (\n    id: string,\n    size: number = 10,\n    pageNumber: number = 1,\n    sort?: string[],\n    filters?: FlaskFilters,\n) => {\n    const queryParameters = [];\n\n    queryParameters.push(`page[size]=${size}`);\n    queryParameters.push(`page[number]=${pageNumber}`);\n\n    if (sort && sort.length > 0) {\n        queryParameters.push(\"sort=\" + sort.join(\",\"));\n    }\n\n    if (filters && filters.length > 0) {\n        queryParameters.push(\"filter=\" + JSON.stringify(filters));\n    }\n\n    return wrappedActionCreator(id, queryParameters);\n};\n"
  },
  {
    "path": "ui/src/forms/ApplicationForm.tsx",
    "content": "import * as React from \"react\";\n// import {Field, FormProps, reduxForm} from \"redux-form\";\n// import Form from \"semantic-ui-react/src/collections/Form\";\n// import Message from \"semantic-ui-react/src/collections/Message\";\n// import Button from \"semantic-ui-react/src/elements/Button\";\n// import Segment from \"semantic-ui-react/src/elements/Segment\";\n//\n// import Icon from \"semantic-ui-react/dist/commonjs/elements/Icon/Icon\";\nimport {Application} from \"../store/applications/types\";\n// import {SemanticCheckbox} from \"./fields/SemanticCheckbox\";\n// import {SemanticInput} from \"./fields/SemanticInput\";\n// import {SemanticTextArea} from \"./fields/SemanticTextArea\";\n// import {httpsURL} from \"../validations\";\n//\nexport interface IFormData extends Application {\n\n}\n//\n// interface IApplicationFormProps extends FormProps<IFormData, any, any> {\n//     onClickFetch: (e: any) => void;\n// }\n//\n// const UnconnectedApplicationForm: React.StatelessComponent<IApplicationFormProps> = (props) => {\n//         const { error, handleSubmit, pristine, reset, submitting } = props;\n//\n//         return (\n//             <Form onSubmit={handleSubmit} error={error}>\n//                 <Message attached>Enterprise Application (.pkg)</Message>\n//                 <Segment attached>\n//                     <Field\n//                         id=\"manifest-url\"\n//                         label=\"Manifest URL\"\n//                         name=\"manifest_url\"\n//                         component={SemanticInput}\n//                         type=\"text\"\n//                         validate={[httpsURL]}\n//                         action={<Button icon labelPosition=\"right\" onClick={this.onClickFetch}>\n//                             <Icon name=\"cloud download\" /> Fetch</Button>}\n//                     />\n//                 </Segment>\n//                 <Segment attached>\n//                     <Form.Group>\n//                         <Field\n//                             id=\"display-name\"\n//                             label=\"Display Name\"\n//                             name=\"display_name\"\n//                             component={SemanticInput}\n//                             width={12}\n//                             type=\"text\" required />\n//                         <Field\n//                             id=\"version\"\n//                             label=\"Version\"\n//                             name=\"version\"\n//                             width={4}\n//                             component={SemanticInput}\n//                             type=\"text\" required />\n//                     </Form.Group>\n//                     {/*<Field*/}\n//                         {/*id=\"itunes-store-id\"*/}\n//                         {/*label=\"iTunes store ID\"*/}\n//                         {/*name=\"itunes_store_id\"*/}\n//                         {/*component={SemanticInput}*/}\n//                         {/*type=\"text\" />*/}\n//                     <Field\n//                         id=\"bundle-id\"\n//                         label=\"Bundle Identifier\"\n//                         name=\"bundle_id\"\n//                         component={SemanticInput}\n//                         type=\"text\" />\n//                     <Field\n//                         id=\"description\"\n//                         label=\"Description\"\n//                         name=\"description\"\n//                         component={SemanticTextArea}\n//                         type=\"TextArea\" />\n//                 </Segment>\n//                 <Message attached>Management Options</Message>\n//                 <Segment attached>\n//                     <Field\n//                         id=\"management-flags-remove-app\"\n//                         label=\"Remove application when MDM profile is removed\"\n//                         name=\"management_flags_remove_app\"\n//                         component={SemanticCheckbox}\n//                         type=\"checkbox\" />\n//                     <Field\n//                         id=\"management-flags-prevent-backup\"\n//                         label=\"App data cannot be backed up to iCloud or iTunes\"\n//                         name=\"management_flags_prevent_backup\"\n//                         component={SemanticCheckbox}\n//                         type=\"checkbox\" />\n//                     <Field\n//                         id=\"change-management-state\"\n//                         label=\"Take management of this application if it is already installed\"\n//                         name=\"change_management_state\"\n//                         component={SemanticCheckbox}\n//                         type=\"checkbox\"\n//                         value=\"Managed\" />\n//                     <Button type=\"submit\" disabled={submitting}>Save</Button>\n//                 </Segment>\n//             </Form>\n//         );\n// };\n//\n// export const ApplicationForm = reduxForm<IFormData, IApplicationFormProps, undefined>({\n//     form: \"application\",\n// })(UnconnectedApplicationForm);\n"
  },
  {
    "path": "ui/src/forms/DeviceGroupForm.tsx",
    "content": "// import * as React from \"react\";\n// import {Field, FormProps, reduxForm} from \"redux-form\";\n// import Form, {FormComponent, FormProps} from \"semantic-ui-react/src/collections/Form\";\n// import Button from \"semantic-ui-react/src/elements/Button\";\n//\n// import {SemanticInput} from \"./fields/SemanticInput\";\n//\n// export interface FormData {\n//     name: string;\n// }\n//\n// interface DeviceGroupFormProps extends FormProps<FormData, any, any> {\n//\n// }\n//\n// class UnconnectedDeviceGroupForm extends React.Component<DeviceGroupFormProps, undefined> {\n//     public render() {\n//         const {\n//             handleSubmit,\n//         } = this.props;\n//\n//         return (\n//             <Form onSubmit={handleSubmit}>\n//                 <Field id=\"name\" label=\"Name\" name=\"name\" component={SemanticInput} type=\"text\" required />\n//                 <Button type=\"submit\">Save</Button>\n//             </Form>\n//         );\n//     }\n// }\n//\n// export const DeviceGroupForm = reduxForm<FormData, DeviceGroupFormProps, undefined>({\n//     form: \"device_group\",\n// })(UnconnectedDeviceGroupForm);\n"
  },
  {
    "path": "ui/src/guards.ts",
    "content": "// NOTE: Does not work with frames (but we don't have any)\nimport {ApiError} from \"redux-api-middleware\";\n\nexport const isArray = (v: any): v is Array<any> => v instanceof Array;\nexport const isApiError = (v: any): v is ApiError => v instanceof ApiError;\n\n//// Type Guards\n// import {ApiError, ErrorNames} from \"redux-api-middleware\";\n//\n// export function isApiError(payload: any): payload is ApiError {\n//     return payload.name && payload.name === ErrorNames.ApiError;\n// }\n"
  },
  {
    "path": "ui/src/hooks/useForm.ts",
    "content": "import { useState } from \"react\";\n\nexport const useForm = (callback) => {\n\n    const [values, setValues] = useState({});\n\n    const handleSubmit = (event) => {\n        if (event) event.preventDefault();\n        callback();\n    };\n\n    const handleChange = (event) => {\n        event.persist();\n        setValues((values) => ({ ...values, [event.target.name]: event.target.value }));\n    };\n\n    return {\n        handleChange,\n        handleSubmit,\n        values,\n    }\n};\n"
  },
  {
    "path": "ui/src/json-api-v1.ts",
    "content": "\nexport const ContentType = \"application/vnd.api+json\";\n\nexport type Link = string | { href?: string, meta?: { [index: string]: any }};\n\nexport interface Links {\n    self?: Link;\n    related?: Link;\n\n    // Pagination\n    first?: Link;\n    last?: Link;\n    prev?: Link;\n    next?: Link;\n}\n\n// http://jsonapi.org/format/#errors\nexport interface ErrorObject {\n    id?: any;\n    links?: {\n        about?: string;\n    };\n    status?: string;\n    code?: string;\n    title?: string;\n    detail?: string;\n    source?: {\n        pointer?: string;\n        parameter?: string;\n    };\n    meta?: any;\n}\n\nexport interface ErrorResponse {\n    errors: ErrorObject[];\n    jsonapi: {\n        version: string;\n    };\n}\n\nexport interface ResourceIdentifier {\n    type: string;\n    id: string;\n    meta?: any;\n}\n\nexport type RelationshipData = ResourceIdentifier[] | ResourceIdentifier | null;\n\nexport interface Relationship {\n    data: RelationshipData,\n    links: Links;\n    meta?: any;\n}\n\nexport interface Relationships {\n    [relationshipName: string]: Relationship;\n}\n\nexport type PrimaryData = ResourceObject<any> | Array<ResourceObject<any>> | null | any[] |\n    ResourceIdentifier | ResourceIdentifier[];\n\nexport interface DataResponse<TData, TIncluded> {\n    data?: TData;\n    included?: TIncluded;\n    links?: Links;\n    meta?: {\n        count?: number;\n    };\n    jsonapi?: {\n        version: string;\n    };\n}\n\nexport interface ResourceObject<TAttributes> {\n    id: string|number;\n    type: string;\n    attributes?: TAttributes;\n    relationships?: Relationships;\n    links?: Links;\n    meta?: any;\n}\n\nexport interface CreateResourceObject<TAttributes> {\n    type: string;\n    attributes?: TAttributes;\n    relationships?: Relationships;\n    links?: Links;\n    meta?: any;\n}\n\nexport type JSONAPIDocument<TData = any, TIncluded = any> = DataResponse<TData, TIncluded> | ErrorResponse;\n\nexport function isErrorResponse(value: JSONAPIDocument): value is ErrorResponse {\n    return value.hasOwnProperty(\"errors\");\n}\n"
  },
  {
    "path": "ui/src/models.ts",
    "content": "\n\nexport interface InstalledPayload {\n    id?: number;\n    description: string;\n    display_name: string;\n    identifier: string;\n    organization: string;\n    payload_type: string;\n    uuid: string;\n}\n\n"
  },
  {
    "path": "ui/src/reducers/index.ts",
    "content": "import {connectRouter, RouterState} from \"connected-react-router\";\nimport {combineReducers} from \"redux\";\n\nimport {applications, IApplicationsState} from \"../store/applications/list_reducer\";\nimport {IManagedApplicationsState, managed_applications} from \"../store/applications/managed_reducer\";\nimport {application, IApplicationState} from \"../store/applications/reducer\";\nimport {assistant, IAssistantState} from \"../store/assistant/reducer\";\nimport {IAuthenticationState, reducer as auth} from \"../store/auth/reducer\";\nimport {certificates, CertificatesState} from \"../store/certificates/reducer\";\nimport {commands, CommandsState} from \"../store/commands/reducer\";\nimport {configuration, ConfigurationState} from \"../store/configuration/reducer\";\nimport {dep, IDEPState} from \"../store/dep/reducer\";\nimport {device, DeviceState} from \"../store/device/reducer\";\nimport {device_groups, DeviceGroupsState} from \"../store/device_groups/reducer\";\nimport {devices, IDevicesState} from \"../store/devices/devices\";\nimport {organization, OrganizationState} from \"../store/organization/reducer\";\nimport {IProfileState, profile} from \"../store/profile/reducer\";\nimport {profiles, ProfilesState} from \"../store/profiles/reducer\";\nimport {ITableState, table} from \"../store/table/reducer\";\nimport {ITagsState, tags} from \"../store/tags/reducer\";\n\nexport interface RootState {\n    router?: RouterState;\n\n    certificates?: CertificatesState;\n    assistant?: IAssistantState;\n    auth?: IAuthenticationState;\n    configuration?: ConfigurationState;\n    organization?: OrganizationState;\n    devices?: IDevicesState;\n    device?: DeviceState;\n    commands?: CommandsState;\n    profiles?: ProfilesState;\n    device_groups?: DeviceGroupsState;\n    tags?: ITagsState;\n    profile?: IProfileState;\n    applications?: IApplicationsState;\n    application?: IApplicationState;\n    managed_applications?: IManagedApplicationsState;\n    dep?: IDEPState;\n    table?: ITableState;\n}\n\nexport const rootReducer = (history: any) => combineReducers<RootState>({\n    application,\n    applications,\n    assistant,\n    auth,\n    certificates,\n    commands,\n    configuration,\n    dep,\n    device,\n    device_groups,\n    devices,\n    managed_applications,\n    organization,\n    profile,\n    profiles,\n    router: connectRouter(history),\n    table,\n    tags,\n});\n\nexport default rootReducer;\n"
  },
  {
    "path": "ui/src/reducers/interfaces.ts",
    "content": "import {ApiError} from \"redux-api-middleware\";\n\n/**\n * This interface declares a common interface for reducers that contain an array of results and metadata about those\n * results.\n */\nexport interface IResults<TResultArray> {\n    items: TResultArray;\n    loading: boolean;\n    error?: ApiError | any;\n    lastReceived?: Date;\n    currentPage: number;\n    pageSize: number;\n    pages: number;\n    recordCount?: number;\n}\n\nexport const ResultsDefaultState: IResults<any> = {\n    currentPage: 1,\n    error: null,\n    items: [],\n    lastReceived: null,\n    loading: false,\n    pageSize: 20,\n    pages: 0,\n    recordCount: 0,\n};\n"
  },
  {
    "path": "ui/src/selectors/device.ts",
    "content": "import { createSelector, Selector as Reselector} from \"reselect\";\nimport {RootState} from \"../reducers/index\";\n\nexport const getDeviceAvailableCapacity = (state: RootState) => state.device.device ? state.device.device.attributes.available_device_capacity : null;\nexport const getDeviceCapacity = (state: RootState) => state.device.device ? state.device.device.attributes.device_capacity : null;\n\nexport const getPercentCapacityUsed: Reselector<RootState, number> = createSelector(\n    [getDeviceCapacity, getDeviceAvailableCapacity],\n    (deviceCapacity: number = 0, availableCapacity: number = 0) => (deviceCapacity - availableCapacity)\n);\n\n"
  },
  {
    "path": "ui/src/store/applications/actions.ts",
    "content": "import * as fetchJsonp from \"fetch-jsonp\";\nimport {Action} from \"redux\";\nimport {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {ThunkAction} from \"redux-thunk\";\nimport {RootState} from \"../../reducers\";\nimport {JSON_HEADERS, JSONAPI_HEADERS} from \"../constants\";\nimport {JSONAPIDetailResponse, JSONAPIErrorResponse, RSAAPatchActionRequest} from \"../json-api\";\nimport {\n    JSONAPIRelationship,\n    JSONAPIRelationships, RSAAChildIndexActionRequest,\n    RSAADeleteActionRequest,\n    RSAADeleteActionResponse,\n    RSAAIndexActionRequest,\n    RSAAIndexActionResponse,\n    RSAAPostActionRequest,\n    RSAAPostActionResponse,\n    RSAAReadActionRequest,\n    RSAAReadActionResponse,\n} from \"../json-api\";\nimport {EntityType, IItunesSearchQuery, IiTunesSearchResult, MediaType} from \"./itunes\";\nimport {ManagedApplicationsActionTypes} from \"./managed\";\nimport {\n    Application,\n    ApplicationRelationship,\n    IOSStoreApplication,\n    MacStoreApplication,\n    ManagedApplication,\n} from \"./types\";\nimport {\n    encodeJSONAPIChildIndexParameters,\n    encodeJSONAPIIndexParameters,\n    FlaskFilter,\n    FlaskFilters\n} from \"../../flask-rest-jsonapi\";\nimport {RelationshipData} from \"../../json-api-v1\";\n\nexport enum ApplicationsActionTypes {\n    INDEX_REQUEST = \"applications/INDEX_REQUEST\",\n    INDEX_SUCCESS = \"applications/INDEX_SUCCESS\",\n    INDEX_FAILURE = \"applications/INDEX_FAILURE\",\n    READ_REQUEST = \"applications/READ_REQUEST\",\n    READ_SUCCESS = \"applications/READ_SUCCESS\",\n    READ_FAILURE = \"applications/READ_FAILURE\",\n    PATCH_REQUEST = \"applications/PATCH_REQUEST\",\n    PATCH_SUCCESS = \"applications/PATCH_SUCCESS\",\n    PATCH_FAILURE = \"applications/PATCH_FAILURE\",\n    POST_REQUEST = \"applications/POST_REQUEST\",\n    POST_SUCCESS = \"applications/POST_SUCCESS\",\n    POST_FAILURE = \"applications/POST_FAILURE\",\n    DELETE_REQUEST = \"applications/DELETE_REQUEST\",\n    DELETE_SUCCESS = \"applications/DELETE_SUCCESS\",\n    DELETE_FAILURE = \"applications/DELETE_FAILURE\",\n\n    REL_PATCH_REQUEST = \"applications/relationships/PATCH_REQUEST\",\n    REL_PATCH_SUCCESS = \"applications/relationships/PATCH_SUCCESS\",\n    REL_PATCH_FAILURE = \"applications/relationships/PATCH_FAILURE\",\n    REL_DELETE_REQUEST = \"applications/relationships/DELETE_REQUEST\",\n    REL_DELETE_SUCCESS = \"applications/relationships/DELETE_SUCCESS\",\n    REL_DELETE_FAILURE = \"applications/relationships/DELETE_FAILURE\",\n\n    MANAGED_REQUEST = \"applications/MANAGED_REQUEST\",\n    MANAGED_SUCCESS = \"applications/MANAGED_SUCCESS\",\n    MANAGED_FAILURE = \"applications/MANAGED_FAILURE\",\n\n    ITUNES_SEARCH_REQUEST = \"applications/ITUNES_SEARCH_REQUEST\",\n    ITUNES_SEARCH_SUCCESS = \"applications/ITUNES_SEARCH_SUCCESS\",\n    ITUNES_SEARCH_FAILURE = \"applications/ITUNES_SEARCH_FAILURE\",\n}\n\nexport type ManagedActionRequest = RSAAChildIndexActionRequest<\n    ApplicationsActionTypes.MANAGED_REQUEST,\n    ApplicationsActionTypes.MANAGED_SUCCESS,\n    ApplicationsActionTypes.MANAGED_FAILURE>;\nexport type ManagedActionResponse = RSAAIndexActionResponse<\n    ApplicationsActionTypes.MANAGED_REQUEST,\n    ApplicationsActionTypes.MANAGED_SUCCESS,\n    ApplicationsActionTypes.MANAGED_FAILURE,\n    ManagedApplication>;\n\nexport const managed = encodeJSONAPIChildIndexParameters((appId: string, queryParameters: string[])  => {\n    return ({\n        [RSAA]: {\n            endpoint: `/api/v1/applications/${appId}/managed_applications?${queryParameters.join(\"&\")}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                ApplicationsActionTypes.MANAGED_REQUEST,\n                ApplicationsActionTypes.MANAGED_SUCCESS,\n                ApplicationsActionTypes.MANAGED_FAILURE,\n            ],\n        },\n    } as RSAAction<\n        ApplicationsActionTypes.MANAGED_REQUEST,\n        ApplicationsActionTypes.MANAGED_SUCCESS,\n        ApplicationsActionTypes.MANAGED_FAILURE>);\n});\n\nexport type IndexActionRequest = RSAAIndexActionRequest<\n    ApplicationsActionTypes.INDEX_REQUEST,\n    ApplicationsActionTypes.INDEX_SUCCESS,\n    ApplicationsActionTypes.INDEX_FAILURE>;\nexport type IndexActionResponse = RSAAIndexActionResponse<\n    ApplicationsActionTypes.INDEX_REQUEST,\n    ApplicationsActionTypes.INDEX_SUCCESS,\n    ApplicationsActionTypes.INDEX_FAILURE,\n    Application>;\n\nexport const index = encodeJSONAPIIndexParameters((queryParameters: string[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/applications?\" + queryParameters.join(\"&\"),\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                ApplicationsActionTypes.INDEX_REQUEST,\n                ApplicationsActionTypes.INDEX_SUCCESS,\n                ApplicationsActionTypes.INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<\n        ApplicationsActionTypes.INDEX_REQUEST,\n        ApplicationsActionTypes.INDEX_SUCCESS,\n        ApplicationsActionTypes.INDEX_FAILURE>);\n});\n\nexport type PostActionRequest = RSAAPostActionRequest<\n    ApplicationsActionTypes.POST_REQUEST,\n    ApplicationsActionTypes.POST_SUCCESS,\n    ApplicationsActionTypes.POST_FAILURE,\n    Application>;\nexport type PostActionResponse = RSAAPostActionResponse<\n    ApplicationsActionTypes.POST_REQUEST,\n    ApplicationsActionTypes.POST_SUCCESS,\n    ApplicationsActionTypes.POST_FAILURE,\n    JSONAPIDetailResponse<Application, undefined>>;\n\nexport const post: PostActionRequest = (values: Application, relationships: RelationshipData) => {\n    return ({\n        [RSAA]: {\n            body: JSON.stringify({\n                data: {\n                    attributes: values,\n                    type: \"applications\",\n                },\n            }),\n            endpoint: `/api/v1/applications`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                ApplicationsActionTypes.POST_REQUEST,\n                ApplicationsActionTypes.POST_SUCCESS,\n                ApplicationsActionTypes.POST_FAILURE,\n            ],\n        },\n    } as RSAAction<\n        ApplicationsActionTypes.POST_REQUEST,\n        ApplicationsActionTypes.POST_SUCCESS,\n        ApplicationsActionTypes.POST_FAILURE>);\n};\n\nexport const postAppStoreMac: PostActionRequest = (values: MacStoreApplication, relationships: JSONAPIRelationships) => {\n    return ({\n        [RSAA]: {\n            body: JSON.stringify({\n                data: {\n                    attributes: values,\n                    type: \"applications\",\n                },\n            }),\n            endpoint: `/api/v1/applications/store/mac`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                ApplicationsActionTypes.POST_REQUEST,\n                ApplicationsActionTypes.POST_SUCCESS,\n                ApplicationsActionTypes.POST_FAILURE,\n            ],\n        },\n    } as RSAAction<\n        ApplicationsActionTypes.POST_REQUEST,\n        ApplicationsActionTypes.POST_SUCCESS,\n        ApplicationsActionTypes.POST_FAILURE>);\n};\n\nexport const postAppStoreIos: PostActionRequest = (values: IOSStoreApplication, relationships: JSONAPIRelationships) => {\n    return ({\n        [RSAA]: {\n            body: JSON.stringify({\n                data: {\n                    attributes: values,\n                    type: \"applications\",\n                },\n            }),\n            endpoint: `/api/v1/applications/store/ios`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                ApplicationsActionTypes.POST_REQUEST,\n                ApplicationsActionTypes.POST_SUCCESS,\n                ApplicationsActionTypes.POST_FAILURE,\n            ],\n        },\n    } as RSAAction<\n        ApplicationsActionTypes.POST_REQUEST,\n        ApplicationsActionTypes.POST_SUCCESS,\n        ApplicationsActionTypes.POST_FAILURE>);\n};\n\nexport type ReadActionRequest = RSAAReadActionRequest<\n    ApplicationsActionTypes.READ_REQUEST,\n    ApplicationsActionTypes.READ_SUCCESS,\n    ApplicationsActionTypes.READ_FAILURE>;\nexport type ReadActionResponse = RSAAReadActionResponse<\n    ApplicationsActionTypes.READ_REQUEST,\n    ApplicationsActionTypes.READ_SUCCESS,\n    ApplicationsActionTypes.READ_FAILURE,\n    JSONAPIDetailResponse<Application, undefined>>;\n\nexport const read: ReadActionRequest = (id: string, include?: string[]) => {\n\n    let inclusions = \"\";\n    if (include && include.length) {\n        inclusions = \"include=\" + include.join(\",\");\n    }\n\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/applications/${id}?${inclusions}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                ApplicationsActionTypes.READ_REQUEST,\n                ApplicationsActionTypes.READ_SUCCESS,\n                ApplicationsActionTypes.READ_FAILURE,\n            ],\n        },\n    };\n};\n\nexport type PatchActionRequest = RSAAPatchActionRequest<\n    ApplicationsActionTypes.PATCH_REQUEST,\n    ApplicationsActionTypes.PATCH_SUCCESS,\n    ApplicationsActionTypes.PATCH_FAILURE,\n    Application>;\nexport type PatchActionResponse = RSAAReadActionResponse<\n    ApplicationsActionTypes.PATCH_REQUEST,\n    ApplicationsActionTypes.PATCH_SUCCESS,\n    ApplicationsActionTypes.PATCH_FAILURE,\n    JSONAPIDetailResponse<Application, undefined>>;\n\nexport const patch: PatchActionRequest = (applicationId: string, values: Application) => {\n\n    return {\n        [RSAA]: {\n            body: JSON.stringify({\n                data: {\n                    attributes: values,\n                    type: \"applications\",\n                },\n            }),\n            endpoint: `/api/v1/applications/${applicationId}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"PATCH\",\n            types: [\n                ApplicationsActionTypes.PATCH_REQUEST,\n                ApplicationsActionTypes.PATCH_SUCCESS,\n                ApplicationsActionTypes.PATCH_FAILURE,\n            ],\n        },\n    };\n};\n\nexport type DeleteActionRequest = RSAADeleteActionRequest<\n    ApplicationsActionTypes.DELETE_REQUEST,\n    ApplicationsActionTypes.DELETE_SUCCESS,\n    ApplicationsActionTypes.DELETE_FAILURE>;\nexport type DeleteActionResponse = RSAADeleteActionResponse<\n    ApplicationsActionTypes.DELETE_REQUEST,\n    ApplicationsActionTypes.DELETE_SUCCESS,\n    ApplicationsActionTypes.DELETE_FAILURE,\n    Application>;\n\nexport const destroy: DeleteActionRequest = (id: string) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/applications/${id}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"DELETE\",\n            types: [\n                ApplicationsActionTypes.DELETE_REQUEST,\n                ApplicationsActionTypes.DELETE_SUCCESS,\n                ApplicationsActionTypes.DELETE_FAILURE,\n            ],\n        },\n    };\n};\n\nexport type PatchRelationshipActionRequest = (\n    applicationId: string,\n    relationship: ApplicationRelationship,\n    data: JSONAPIRelationship[],\n) => RSAAction<\n    ApplicationsActionTypes.REL_PATCH_REQUEST,\n    ApplicationsActionTypes.REL_PATCH_SUCCESS,\n    ApplicationsActionTypes.REL_PATCH_FAILURE>;\n\nexport type PatchRelationshipActionResponse = RSAAReadActionResponse<\n    ApplicationsActionTypes.REL_PATCH_REQUEST,\n    ApplicationsActionTypes.REL_PATCH_SUCCESS,\n    ApplicationsActionTypes.REL_PATCH_FAILURE,\n    JSONAPIDetailResponse<Application, undefined>>;\n\nexport const patchRelationship: PatchRelationshipActionRequest = (\n    applicationId: string, relationship: ApplicationRelationship, data: JSONAPIRelationship[]) => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify({ data }),\n            endpoint: `/api/v1/applications/${applicationId}/relationships/${relationship}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"PATCH\",\n            types: [\n                ApplicationsActionTypes.REL_PATCH_REQUEST,\n                ApplicationsActionTypes.REL_PATCH_SUCCESS,\n                ApplicationsActionTypes.REL_PATCH_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type DeleteRelationshipActionRequest = (\n    applicationId: string,\n    relationship: ApplicationRelationship,\n    data: JSONAPIRelationship[],\n) => RSAAction<\n    ApplicationsActionTypes.REL_DELETE_REQUEST,\n    ApplicationsActionTypes.REL_DELETE_SUCCESS,\n    ApplicationsActionTypes.REL_DELETE_FAILURE>;\n\nexport type DeleteRelationshipActionResponse = RSAAReadActionResponse<\n    ApplicationsActionTypes.REL_DELETE_REQUEST,\n    ApplicationsActionTypes.REL_DELETE_SUCCESS,\n    ApplicationsActionTypes.REL_DELETE_FAILURE,\n    JSONAPIDetailResponse<Application, undefined>>;\n\nexport const deleteRelationship: DeleteRelationshipActionRequest = (\n    applicationId: string, relationship: ApplicationRelationship, data: JSONAPIRelationship[]) => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify({ data }),\n            endpoint: `/api/v1/applications/${applicationId}/relationships/${relationship}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"DELETE\",\n            types: [\n                ApplicationsActionTypes.REL_DELETE_REQUEST,\n                ApplicationsActionTypes.REL_DELETE_SUCCESS,\n                ApplicationsActionTypes.REL_DELETE_FAILURE,\n            ],\n        },\n    }\n};\n\nexport interface ITunesSearchRequestAction extends Action<ApplicationsActionTypes.ITUNES_SEARCH_REQUEST> {\n    payload: IItunesSearchQuery;\n}\n\nexport interface ITunesSearchSuccessAction extends Action<ApplicationsActionTypes.ITUNES_SEARCH_SUCCESS> {\n    payload: IiTunesSearchResult;\n}\n\nexport interface ITunesSearchFailureAction extends Action<ApplicationsActionTypes.ITUNES_SEARCH_FAILURE> {\n    error: boolean;\n    errorDetail: any;\n}\n\nexport type ItunesSearchActions = ITunesSearchRequestAction | ITunesSearchSuccessAction | ITunesSearchFailureAction;\nexport type ItunesSearchAction = (\n    term: string,\n    country: string,\n    media: MediaType,\n    entity: EntityType,\n    limit?: number) => ThunkAction<void, RootState, void, ItunesSearchActions>;\n\nexport const itunesSearch: ItunesSearchAction = (\n    term: string,\n    country: string,\n    media: MediaType,\n    entity: EntityType,\n    limit?: number) => (dispatch, getState) => {\n\n    dispatch({\n        payload: { term, country, media, entity, limit },\n        type: ApplicationsActionTypes.ITUNES_SEARCH_REQUEST,\n    });\n\n    const query = `term=${term}&country=${country}&entity=${entity}`;\n\n    fetchJsonp(`https://itunes.apple.com/search?${query}`).then((response) => {\n        return response.json();\n    }).then((json) => {\n        dispatch({\n            payload: json,\n            type: ApplicationsActionTypes.ITUNES_SEARCH_SUCCESS,\n        });\n    }).catch((e) => {\n        dispatch({\n            error: true,\n            errorDetail: e,\n            type: ApplicationsActionTypes.ITUNES_SEARCH_FAILURE,\n        })\n    });\n};\n\nexport type ApplicationsActions = IndexActionResponse | PostActionResponse | PatchActionResponse |\n    ReadActionResponse | PatchRelationshipActionResponse | DeleteRelationshipActionResponse | ItunesSearchActions;\n"
  },
  {
    "path": "ui/src/store/applications/itunes.ts",
    "content": "// iTunes Search API\n// Structures taken from Documentation, available at:\n// https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/\n\nexport const ITUNES_SEARCH_URL: string = \"https://itunes.apple.com/search\";\n\nexport enum MediaType {\n    movie = \"movie\",\n    podcast = \"podcast\",\n    music = \"music\",\n    musicVideo = \"musicVideo\",\n    audiobook = \"audiobook\",\n    shortFilm = \"shortFilm\",\n    tvShow = \"tvShow\",\n    software = \"software\",\n    ebook = \"ebook\",\n    all = \"all\",\n}\n\nexport enum EntityType {\n    movieArtist = \"movieArtist\",\n    movie = \"movie\",\n\n    podcastAuthor = \"podcastAuthor\",\n    podcast = \"podcast\",\n\n    musicArtist = \"musicArtist\",\n    musicTrack = \"musicTrack\",\n    album = \"album\",\n    musicVideo = \"musicVideo\",\n    mix = \"mix\",\n    song = \"song\",\n\n    audiobookAuthor = \"audiobookAuthor\",\n    audiobook = \"audiobook\",\n\n    shortFilmArtist = \"shortFilmArtist\",\n    shortFilm = \"shortFilm\",\n\n    tvEpisode = \"tvEpisode\",\n    tvSeason = \"tvSeason\",\n\n    software = \"software\",\n    iPadSoftware = \"iPadSoftware\",\n    macSoftware = \"macSoftware\",\n\n    ebook = \"ebook\",\n\n    allArtist = \"allArtist\",\n    allTrack = \"allTrack\",\n}\n\nexport interface IMediaTypeEntityList {\n    [propName: string]: string[];\n}\n\nexport const MediaTypeEntities: IMediaTypeEntityList = {\n    [MediaType.software]: [\n        \"software\",\n        \"iPadSoftware\",\n        \"macSoftware\",\n    ],\n};\n\nexport enum MovieAttribute {\n    actorTerm = \"actorTerm\",\n    genreIndex = \"genreIndex\",\n    artistTerm = \"artistTerm\",\n    shortFilmTerm = \"shortFilmTerm\",\n    producerTerm = \"producerTerm\",\n    ratingTerm = \"ratingTerm\",\n    directorTerm = \"directorTerm\",\n    releaseYearTerm = \"releaseYearTerm\",\n    featureFilmTerm = \"featureFilmTerm\",\n    movieArtistTerm = \"movieArtistTerm\",\n    movieTerm = \"movieTerm\",\n    ratingIndex = \"ratingIndex\",\n    descriptionTerm = \"descriptionTerm\",\n}\n\nexport enum ArtworkIconSize {\n    Sixty = \"artworkUrl60\",\n    Hundred = \"artworkUrl100\",\n    FiveTwelve = \"artworkUrl512\",\n}\n\nexport interface IItunesSearchQuery {\n    term: string;\n    country: string;\n    media?: MediaType;\n    entity?: EntityType;\n    attribute?: string;\n    callback?: string;\n    limit?: number;\n    lang?: string;\n    version?: number;\n    explicit?: string;\n}\n\nexport interface IiTunesSearchResult {\n    resultCount: number;\n    results: IiTunesSoftwareSearchResult[];\n}\n\nexport enum IiTunesSoftwareKind {\n    software = \"software\",\n    macSoftware = \"mac-software\",\n}\n\nexport interface IiTunesSoftwareSearchResult {\n    isGameCenterEnabled: boolean;\n    ipadScreenshotUrls: string[];\n    screenshotUrls: string[];\n    appletvScreenshotUrls: string[];\n    artworkUrl60: string;\n    artworkUrl512: string;\n    artworkUrl100: string;\n    artistViewUrl: string;\n    advisories: string[];\n    supportedDevices: string[];\n    kind: IiTunesSoftwareKind;\n    features: string[];\n    trackCensoredName: string;\n    languageCodesISO2A: string[];\n    fileSizeBytes: number;\n    sellerUrl: string;\n    contentAdvisoryRating: string;\n    trackViewUrl: string;\n    trackContentRating: string;\n    releaseNotes: string;\n    formattedPrice: string;\n    trackName: string;\n    primaryGenreName: string;\n    genreIds: string[];\n    sellerName: string;\n    releaseDate: string;  // eg 2016-11-04T19:34:13Z\n    primaryGenreId: number;\n    isVppDeviceBasedLicensingEnabled: boolean;\n    currency: string;\n    wrapperType: string;\n    version: string;\n    trackId: number;\n    description: string;\n    artistId: number;\n    artistName: string;\n    genres: string[];\n    price: number;\n    minimumOsVersion: string;\n    bundleId: string;\n    currentVersionReleaseDate: string;\n    averageUserRating: number;\n    userRatingCount: number;\n}\n"
  },
  {
    "path": "ui/src/store/applications/list_reducer.ts",
    "content": "import {IResults, ResultsDefaultState} from \"../../reducers/interfaces\";\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\nimport {ApplicationsActions, ApplicationsActionTypes} from \"./actions\";\nimport {Application} from \"./types\";\nimport {IiTunesSearchResult} from \"./itunes\";\n\nexport interface IApplicationsState extends IResults<Array<JSONAPIDataObject<Application>>> {\n    allIds: string[];\n    itunesSearchResult: IiTunesSearchResult;\n    itunesSearchResultLoading: boolean;\n    itunesStoreIdsAdded: number[];\n    storeCountry: string;\n}\n\nconst initialState: IApplicationsState = {\n    ...ResultsDefaultState,\n    allIds: [],\n    itunesSearchResult: null,\n    itunesSearchResultLoading: false,\n    itunesStoreIdsAdded: [],\n    storeCountry: \"AU\",\n};\n\nexport function applications(state: IApplicationsState = initialState,\n                             action: ApplicationsActions): IApplicationsState {\n    switch (action.type) {\n        case ApplicationsActionTypes.INDEX_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case ApplicationsActionTypes.INDEX_FAILURE:\n            return {\n                ...state,\n                error: action.payload,\n            };\n        case ApplicationsActionTypes.INDEX_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: action.payload,\n                    loading: false,\n                };\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    lastReceived: new Date(),\n                    loading: false,\n                    recordCount: action.payload.meta.count,\n                };\n            }\n        case ApplicationsActionTypes.POST_REQUEST:\n            return state;\n        case ApplicationsActionTypes.POST_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: action.payload,\n                    loading: false,\n                };\n            } else {\n                return {\n                    ...state,\n                    itunesStoreIdsAdded: [\n                        ...state.itunesStoreIdsAdded,\n                        action.payload.data.attributes.itunes_store_id],\n                };\n            }\n        case ApplicationsActionTypes.ITUNES_SEARCH_REQUEST:\n            return {\n                ...state,\n                itunesSearchResult: null,\n                itunesSearchResultLoading: true,\n            };\n\n        case ApplicationsActionTypes.ITUNES_SEARCH_SUCCESS:\n            return {\n                ...state,\n                itunesSearchResult: action.payload,\n                itunesSearchResultLoading: false,\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/applications/managed.ts",
    "content": "import {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {JSONAPI_HEADERS} from \"../constants\";\nimport {\n    RSAAChildIndexActionRequest,\n    RSAAIndexActionRequest,\n    RSAAIndexActionResponse\n} from \"../json-api\";\nimport {ManagedApplication} from \"./types\";\nimport {Command} from \"../device/types\";\nimport {DevicesActionTypes} from \"../device/actions\";\nimport {encodeJSONAPIChildIndexParameters, encodeJSONAPIIndexParameters} from \"../../flask-rest-jsonapi\";\n\nexport enum ManagedApplicationsActionTypes {\n    INDEX_REQUEST = \"managed_applications/INDEX_REQUEST\",\n    INDEX_SUCCESS = \"managed_applications/INDEX_SUCCESS\",\n    INDEX_FAILURE = \"managed_applications/INDEX_FAILURE\",\n\n    DEVICES_REQUEST = \"managed_applications/DEVICES_REQUEST\",\n    DEVICES_SUCCESS = \"managed_applications/DEVICES_SUCCESS\",\n    DEVICES_FAILURE = \"managed_applications/DEVICES_FAILURE\",\n}\n\nexport type IndexActionRequest = RSAAIndexActionRequest<\n    ManagedApplicationsActionTypes.INDEX_REQUEST,\n    ManagedApplicationsActionTypes.INDEX_SUCCESS,\n    ManagedApplicationsActionTypes.INDEX_FAILURE>;\n\nexport type IndexActionResponse = RSAAIndexActionResponse<\n    ManagedApplicationsActionTypes.INDEX_REQUEST,\n    ManagedApplicationsActionTypes.INDEX_SUCCESS,\n    ManagedApplicationsActionTypes.INDEX_FAILURE,\n    ManagedApplication>;\n\nexport const index = encodeJSONAPIIndexParameters((queryParameters: string[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/managed_applications?\" + queryParameters.join(\"&\"),\n            headers: JSONAPI_HEADERS,\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                ManagedApplicationsActionTypes.INDEX_REQUEST,\n                ManagedApplicationsActionTypes.INDEX_SUCCESS,\n                ManagedApplicationsActionTypes.INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<ManagedApplicationsActionTypes.INDEX_REQUEST,\n        ManagedApplicationsActionTypes.INDEX_SUCCESS,\n        ManagedApplicationsActionTypes.INDEX_FAILURE>);\n});\n\nexport type DevicesActionRequest = RSAAChildIndexActionRequest<\n    ManagedApplicationsActionTypes.DEVICES_REQUEST,\n    ManagedApplicationsActionTypes.DEVICES_SUCCESS,\n    ManagedApplicationsActionTypes.DEVICES_FAILURE>;\nexport type DevicesActionResponse = RSAAIndexActionResponse<\n    ManagedApplicationsActionTypes.DEVICES_REQUEST,\n    ManagedApplicationsActionTypes.DEVICES_SUCCESS,\n    ManagedApplicationsActionTypes.DEVICES_FAILURE,\n    Command>;\n\nexport const devices = encodeJSONAPIChildIndexParameters((managedAppId: string, queryParameters: string[])  => {\n    return ({\n        [RSAA]: {\n            endpoint: `/api/v1/managed_applications/${managedAppId}/devices?${queryParameters.join(\"&\")}`,\n            headers: JSONAPI_HEADERS,\n            method: \"GET\",\n            types: [\n                ManagedApplicationsActionTypes.DEVICES_REQUEST,\n                ManagedApplicationsActionTypes.DEVICES_SUCCESS,\n                ManagedApplicationsActionTypes.DEVICES_FAILURE,\n            ],\n        },\n    } as RSAAction<\n        ManagedApplicationsActionTypes.DEVICES_REQUEST,\n        ManagedApplicationsActionTypes.DEVICES_SUCCESS,\n        ManagedApplicationsActionTypes.DEVICES_FAILURE>);\n});\n\nexport type ManagedApplicationsActions = IndexActionResponse;\n"
  },
  {
    "path": "ui/src/store/applications/managed_reducer.ts",
    "content": "import {Reducer} from \"redux\";\nimport {IResults, ResultsDefaultState} from \"../../reducers/interfaces\";\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\nimport {ManagedApplicationsActions, ManagedApplicationsActionTypes} from \"./managed\";\nimport {ManagedApplication} from \"./types\";\n\nexport interface IManagedApplicationsState extends IResults<Array<JSONAPIDataObject<ManagedApplication>>> {\n\n}\n\nconst initialState: IManagedApplicationsState = {\n    ...ResultsDefaultState,\n};\n\nexport const managed_applications: Reducer<IManagedApplicationsState, ManagedApplicationsActions> =\n    (state = initialState, action) => {\n    switch (action.type) {\n        case ManagedApplicationsActionTypes.INDEX_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case ManagedApplicationsActionTypes.INDEX_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: action.payload,\n                    loading: false,\n                };\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    loading: false,\n                    recordCount: action.payload.meta.count,\n                };\n            }\n        case ManagedApplicationsActionTypes.INDEX_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n                loading: false,\n            };\n        default:\n            return state;\n    }\n};\n"
  },
  {
    "path": "ui/src/store/applications/reducer.ts",
    "content": "import {JSONAPIDetailResponse} from \"../json-api\";\nimport {ApplicationsActions, ApplicationsActionTypes} from \"./actions\";\nimport {Application} from \"./types\";\n\nexport interface IApplicationState {\n    loading: boolean;\n    data: JSONAPIDetailResponse<Application, void>;\n    error: boolean;\n    errorDetail: any;\n}\n\nconst initialState: IApplicationState = {\n    data: null,\n    error: false,\n    errorDetail: null,\n    loading: false,\n};\n\nexport function application(state: IApplicationState = initialState, action: ApplicationsActions) {\n    switch (action.type) {\n        case ApplicationsActionTypes.READ_REQUEST:\n            return {\n                ...state,\n                data: null,\n                error: false,\n                errorDetail: null,\n                loading: true,\n            };\n        case ApplicationsActionTypes.READ_SUCCESS:\n            return {\n                ...state,\n                data: action.payload,\n                loading: false,\n            };\n        case ApplicationsActionTypes.READ_FAILURE:\n            return {\n                ...state,\n                data: null,\n                error: true,\n                errorDetail: action.payload,\n                loading: false,\n            };\n        case ApplicationsActionTypes.REL_PATCH_REQUEST:\n            return {\n                ...state,\n            };\n        case ApplicationsActionTypes.REL_PATCH_SUCCESS:\n            return state;\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/applications/types.ts",
    "content": "// valid relationship types\nexport type ApplicationRelationship = \"tags\";\n\nexport enum ApplicationDiscriminator {\n    AppStoreMac = \"appstore_mac\",\n    AppStoreIOS = \"appstore_ios\",\n}\n\nexport interface Application {\n    id?: string;\n    itunes_store_id?: number;\n    bundle_id?: string;\n    purchase_method?: number;\n    manifest_url: string;\n    management_flags: number;\n    change_management_state: \"Managed\" | null;\n    display_name: string;\n    description: string;\n    version: string;\n\n    country: string;\n    artist_id: number;\n    artist_name: string;\n    artist_view_url: string;\n    artwork_url60: string;\n    artwork_url100: string;\n    artwork_url512: string;\n    release_notes: string;\n    release_date: string;\n    minimum_os_version: string;\n    file_size_bytes: number;\n}\n\nexport interface MacStoreApplication extends Application {\n    discriminator: ApplicationDiscriminator.AppStoreMac;\n}\n\nexport interface IOSStoreApplication extends Application {\n    discriminator: ApplicationDiscriminator.AppStoreIOS;\n}\n\nexport enum ManagedApplicationStatus {\n    NeedsRedemption = \"NeedsRedemption\",\n    Redeeming = \"Redeeming\",\n    Prompting = \"Prompting\",\n    PromptingForLogin = \"PromptingForLogin\",\n    Installing = \"Installing\",\n    ValidatingPurchase = \"ValidatingPurchase\",\n    Managed = \"Managed\",\n    ManagedButUninstalled = \"ManagedButUninstalled\",\n    PromptingForUpdate = \"PromptingForUpdate\",\n    PromptingForUpdateLogin = \"PromptingForUpdateLogin\",\n    PromptingForManagement = \"PromptingForManagement\",\n    Updating = \"Updating\",\n    ValidatingUpdate = \"ValidatingUpdate\",\n    Unknown = \"Unknown\",\n    UserInstalledApp = \"UserInstalledApp\",\n    UserRejected = \"UserRejected\",\n    UpdateRejected = \"UpdateRejected\",\n    ManagementRejected = \"ManagementRejected\",\n    Failed = \"Failed\",\n    Queued = \"Queued\",\n}\n\nexport interface ManagedApplication {\n    id: string;\n    bundle_id: string;\n    external_version_id?: number;\n    has_configuration: boolean;\n    has_feedback: boolean;\n    is_validated: boolean;\n    management_flags: number;\n    status: ManagedApplicationStatus;\n}\n"
  },
  {
    "path": "ui/src/store/assistant/actions.ts",
    "content": "import {ThunkAction} from 'redux-thunk';\n\nexport type NEXT_STEP = 'assistant/NEXT_STEP';\nexport const NEXT_STEP: NEXT_STEP = 'assistant/NEXT_STEP';\n\nexport type PREV_STEP = 'assistant/PREV_STEP';\nexport const PREV_STEP: PREV_STEP = 'assistant/PREV_STEP';\n\nexport interface NextStepAction {\n    type: NEXT_STEP;\n}\n\nexport interface PrevStepAction {\n    type: PREV_STEP;\n}\n\nexport const nextStep = (): NextStepAction => {\n    return {\n        type: NEXT_STEP\n    };\n};\n\nexport const prevStep = (): PrevStepAction => {\n    return {\n        type: PREV_STEP\n    };\n};\n\n"
  },
  {
    "path": "ui/src/store/assistant/reducer.ts",
    "content": "import * as actions from \"./actions\";\n\nexport interface IAssistantState {\n    currentStep: number;\n    totalSteps: number;\n}\n\nconst initialState: IAssistantState = {\n    currentStep: 0,\n    totalSteps: 0,\n};\n\nexport type AssistantAction = actions.NextStepAction | actions.PrevStepAction;\n\nexport function assistant(state: IAssistantState = initialState, action: AssistantAction): IAssistantState {\n    switch (action.type) {\n        case actions.NEXT_STEP:\n            return {\n                ...state,\n                currentStep: (state.currentStep + 1),\n            };\n        case actions.PREV_STEP:\n            return {\n                ...state,\n                currentStep: (state.currentStep - 1),\n            };\n        default:\n            return state\n    }\n}\n"
  },
  {
    "path": "ui/src/store/auth/actions.ts",
    "content": "import { RSAA, RSAAction } from \"redux-api-middleware\";\nimport {JSONAPI_HEADERS, OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET} from \"../constants\";\nimport {IOAuth2TokenSuccessResponse} from \"./types\";\nimport {ThunkAction} from \"redux-thunk\";\nimport {RootState} from \"../../reducers\";\nimport {Action, ActionCreator} from \"redux\";\n\nexport enum AuthenticationActionTypes {\n    TOKEN_REQUEST = \"authentication/TOKEN_REQUEST\",\n    TOKEN_SUCCESS = \"authentication/TOKEN_SUCCESS\",\n    TOKEN_FAILURE = \"authentication/TOKEN_FAILURE\",\n\n    TOKEN_SAVE = \"authentication/TOKEN_SAVE\",\n}\n\nexport type TokenActionRequest = RSAAction<\n    AuthenticationActionTypes.TOKEN_REQUEST,\n    AuthenticationActionTypes.TOKEN_SUCCESS,\n    AuthenticationActionTypes.TOKEN_FAILURE>;\n\nexport type TokenActionRequestCreator = (email: string, password: string) => TokenActionRequest;\n\nexport interface ITokenActionResponse {\n    type: AuthenticationActionTypes.TOKEN_REQUEST |\n          AuthenticationActionTypes.TOKEN_SUCCESS |\n          AuthenticationActionTypes.TOKEN_FAILURE;\n    payload?: IOAuth2TokenSuccessResponse;\n}\n\nexport const createToken: TokenActionRequestCreator = (email: string, password: string) => {\n    const queryParameters: string[] = [\"grant_type=password\", \"response_type=token\"];\n        // ,\"client_id=F8955645-A21D-44AE-9387-42B0800ADF15\", \"client_secret=A\"];\n    const body: FormData = new FormData();\n    // body.append(\"grant_type\", \"password\");\n    body.append(\"username\", email);\n    body.append(\"password\", password);\n\n    return {\n        [RSAA]: {\n            body,\n            endpoint: \"/oauth/token?\" + queryParameters.join(\"&\"),\n            headers: {\n                Accept: \"application/json\",\n                Authorization: \"Basic \" + btoa(OAUTH2_CLIENT_ID + \":\" + OAUTH2_CLIENT_SECRET),\n            },\n            method: \"POST\",\n            types: [\n                AuthenticationActionTypes.TOKEN_REQUEST,\n                AuthenticationActionTypes.TOKEN_SUCCESS,\n                AuthenticationActionTypes.TOKEN_FAILURE,\n            ],\n        },\n    }\n};\n\nexport interface ITokenSaveRequest extends Action<AuthenticationActionTypes.TOKEN_SAVE> {\n    token: string;\n}\n\nexport const saveToken: ActionCreator<ITokenSaveRequest> = (payload) => {\n    sessionStorage.setItem(\"cmdmnt-token\", payload.access_token);\n\n    return {\n        ...payload,\n        type: AuthenticationActionTypes.TOKEN_SAVE,\n    };\n};\n\nexport const login = (email: string, password: string): ThunkAction<void, RootState, null, TokenActionRequest> =>\n    (dispatch, getState) => {\n\n    return dispatch(createToken(email, password)).then((res) => {\n        console.log(res);\n        dispatch(saveToken(res.payload));\n    }).then(() => {\n        console.log(\"saved token\");\n    })\n};\n\nexport type AuthenticationActions = ITokenActionResponse | Action<AuthenticationActionTypes.TOKEN_SAVE>;\n"
  },
  {
    "path": "ui/src/store/auth/reducer.ts",
    "content": "import {AuthenticationActions, AuthenticationActionTypes} from \"./actions\";\nimport {isApiError} from \"../../guards\";\nimport {ApiError} from \"redux-api-middleware\";\n\nexport interface IAuthenticationState {\n    // In reality, nobody should store the secret client side, but we need to establish an authentication system before\n    // returning to using a more secure method.\n    oauth2_client_id: string;\n    oauth2_client_secret: string;\n\n    access_token?: string;\n    expires_in?: number;\n    token_type?: string;\n\n    loading: boolean;\n\n    error?: ApiError;\n}\n\nconst initialState: IAuthenticationState = {\n    access_token: sessionStorage.getItem(\"cmdmnt-token\"),\n    error: null,\n    expires_in: 0,\n    loading: false,\n    oauth2_client_id: \"F8955645-A21D-44AE-9387-42B0800ADF15\",\n    oauth2_client_secret: \"dummyvalue\",\n    token_type: null,\n};\n\nexport function reducer(state: IAuthenticationState = initialState, action: AuthenticationActions) {\n    switch (action.type) {\n        case AuthenticationActionTypes.TOKEN_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case AuthenticationActionTypes.TOKEN_SUCCESS:\n            return {\n                ...state,\n                access_token: action.payload.access_token,\n                expires_in: action.payload.expires_in,\n                loading: false,\n                token_type: action.payload.token_type,\n            };\n        case AuthenticationActionTypes.TOKEN_FAILURE:\n            let err = null;\n\n            if (isApiError(action.payload)) {\n                err = action.payload;\n            }\n            return {\n                ...state,\n                error: err,\n                loading: false,\n            };\n        case AuthenticationActionTypes.TOKEN_SAVE:\n            return state;\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/auth/types.ts",
    "content": "export interface IOAuth2TokenSuccessResponse {\n    access_token: string;\n    expires_in: number;\n    token_type: \"Bearer\";\n}\n"
  },
  {
    "path": "ui/src/store/certificates/actions.ts",
    "content": "import { RSAA, RSAAction } from \"redux-api-middleware\";\nimport {JSONAPIDataObject, JSONAPIDetailResponse, JSONAPIListResponse} from \"../json-api\";\nimport {JSONAPI_HEADERS} from \"../constants\"\nimport {Certificate} from \"./types\";\nimport {FlaskFilter, FlaskFilters} from \"../../flask-rest-jsonapi\";\n\nexport type INDEX_REQUEST = \"certificates/INDEX_REQUEST\";\nexport const INDEX_REQUEST: INDEX_REQUEST = \"certificates/INDEX_REQUEST\";\nexport type INDEX_SUCCESS = \"certificates/INDEX_SUCCESS\";\nexport const INDEX_SUCCESS: INDEX_SUCCESS = \"certificates/INDEX_SUCCESS\";\nexport type INDEX_FAILURE = \"certificates/INDEX_FAILURE\";\nexport const INDEX_FAILURE: INDEX_FAILURE = \"certificates/INDEX_FAILURE\";\n\ntype IndexActionRequest = (size?: number, number?: number, sort?: string[], filter?: FlaskFilter[]) => RSAAction<INDEX_REQUEST, INDEX_SUCCESS, INDEX_FAILURE>;\n\nexport interface IndexActionResponse {\n    type: INDEX_REQUEST | INDEX_FAILURE | INDEX_SUCCESS;\n    payload?: JSONAPIListResponse<JSONAPIDataObject<Certificate>>;\n}\n\nexport const index: IndexActionRequest = (\n    size: number = 50,\n    number: number = 1,\n    sort: string[] = [],\n    filter?: FlaskFilter[],\n): RSAAction<INDEX_REQUEST, INDEX_SUCCESS, INDEX_FAILURE> => {\n\n    const queryParameters = [];\n    queryParameters.push(`size=${size}`);\n    queryParameters.push(`number=${number}`);\n\n    if (sort.length > 0) {\n        // TODO: sorting\n    }\n\n    if (filter && filter.length > 0) {\n        const rawFilters = JSON.stringify(filter);\n        queryParameters.push(`filter=${rawFilters}`);\n    }\n\n    return {\n        [RSAA]: {\n            endpoint: \"/api/v1/certificates/?\" + queryParameters.join(\"&\"),\n            headers: JSONAPI_HEADERS,\n            method: \"GET\",\n            types: [\n                INDEX_REQUEST,\n                INDEX_SUCCESS,\n                INDEX_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type CERTTYPE_REQUEST = \"certificates/CERTTYPE_REQUEST\";\nexport const CERTTYPE_REQUEST: CERTTYPE_REQUEST = \"certificates/CERTTYPE_REQUEST\";\nexport type CERTTYPE_SUCCESS = \"certificates/CERTTYPE_SUCCESS\";\nexport const CERTTYPE_SUCCESS: CERTTYPE_SUCCESS = \"certificates/CERTTYPE_SUCCESS\";\nexport type CERTTYPE_FAILURE = \"certificates/CERTTYPE_FAILURE\";\nexport const CERTTYPE_FAILURE: CERTTYPE_FAILURE = \"certificates/CERTTYPE_FAILURE\";\n\ntype FetchCertificateTypeActionRequest = (certType: string) => RSAAction<CERTTYPE_REQUEST, CERTTYPE_SUCCESS, CERTTYPE_FAILURE>;\n\nexport interface FetchCertificateTypeActionResponse {\n    type: CERTTYPE_REQUEST | CERTTYPE_SUCCESS | CERTTYPE_FAILURE;\n    payload?: JSONAPIDetailResponse<Certificate, undefined>;\n}\n\nexport const fetchCertificatesForType: FetchCertificateTypeActionRequest = (certType: string): RSAAction<CERTTYPE_REQUEST, CERTTYPE_SUCCESS, CERTTYPE_FAILURE> => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/certificates/type/${certType}?include=private_key`,\n            headers: JSONAPI_HEADERS,\n            method: \"GET\",\n            types: [\n                CERTTYPE_REQUEST,\n                CERTTYPE_SUCCESS,\n                CERTTYPE_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type DELETE_REQUEST = \"certificates/DELETE_REQUEST\";\nexport const DELETE_REQUEST: DELETE_REQUEST = \"certificates/DELETE_REQUEST\";\nexport type DELETE_SUCCESS = \"certificates/DELETE_SUCCESS\";\nexport const DELETE_SUCCESS: DELETE_SUCCESS = \"certificates/DELETE_SUCCESS\";\nexport type DELETE_FAILURE = \"certificates/DELETE_FAILURE\";\nexport const DELETE_FAILURE: DELETE_FAILURE = \"certificates/DELETE_FAILURE\";\n\ntype DeleteCertificateActionRequest = (id: number) => RSAAction<DELETE_REQUEST, DELETE_SUCCESS, DELETE_FAILURE>;\n\nexport interface DeleteCertificateActionResponse {\n    type: DELETE_REQUEST | DELETE_SUCCESS | DELETE_FAILURE;\n    payload?: any;\n}\n\nexport const remove: DeleteCertificateActionRequest = (id: number): RSAAction<DELETE_REQUEST, DELETE_SUCCESS, DELETE_FAILURE> => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/certificates/${id}`,\n            headers: JSONAPI_HEADERS,\n            method: \"DELETE\",\n            types: [\n                DELETE_REQUEST,\n                DELETE_SUCCESS,\n                DELETE_FAILURE,\n            ],\n        },\n    }\n};\n"
  },
  {
    "path": "ui/src/store/certificates/ca_actions.ts",
    "content": "import { RSAA, RSAAction } from 'redux-api-middleware';\nimport {JSONAPI_HEADERS} from '../../store/constants'\nimport {JSONAPIListResponse, JSONAPIDataObject} from \"../json-api\";\nimport {Certificate} from \"../../store/certificates/types\";\nimport {FlaskFilter, FlaskFilters} from \"../../flask-rest-jsonapi\";\n\n\nexport type CACERT_REQUEST = 'certificates/CACERT_REQUEST';\nexport const CACERT_REQUEST: CACERT_REQUEST = 'certificates/CACERT_REQUEST';\nexport type CACERT_SUCCESS = 'certificates/CACERT_SUCCESS';\nexport const CACERT_SUCCESS: CACERT_SUCCESS = 'certificates/CACERT_SUCCESS';\nexport type CACERT_FAILURE = 'certificates/CACERT_FAILURE';\nexport const CACERT_FAILURE: CACERT_FAILURE = 'certificates/CACERT_FAILURE';\n\nexport interface FetchCACertificatesActionRequest {\n    (): RSAAction<CACERT_REQUEST, CACERT_SUCCESS, CACERT_FAILURE>;\n}\n\nexport interface FetchCACertificatesActionResponse {\n    type: CACERT_REQUEST | CACERT_SUCCESS | CACERT_FAILURE;\n    payload?: JSONAPIListResponse<JSONAPIDataObject<Certificate>>;\n}\n\nexport const fetchCACertificates: FetchCACertificatesActionRequest = (): RSAAction<CACERT_REQUEST, CACERT_SUCCESS, CACERT_FAILURE> => {\n    return {\n        [RSAA]: {\n            endpoint: '/api/v1/ca_certificates',\n            method: 'GET',\n            types: [\n                CACERT_REQUEST,\n                CACERT_SUCCESS,\n                CACERT_FAILURE\n            ],\n            headers: JSONAPI_HEADERS\n        }\n    }\n};"
  },
  {
    "path": "ui/src/store/certificates/ca_reducer.ts",
    "content": "import {JSONAPIDataObject, JSONAPIListResponse} from \"../json-api\";\nimport * as actions from \"./ca_actions\";\nimport {Certificate} from \"./types\";\n\nexport interface CAState {\n    items?: JSONAPIListResponse<JSONAPIDataObject<Certificate>>;\n    loading: boolean;\n    error: boolean;\n    errorDetail?: any\n    lastReceived?: Date;\n}\n\nconst initialState: CAState = {\n    loading: false,\n    error: false,\n    errorDetail: null,\n    lastReceived: null,\n};\n\nexport type PushAction = actions.FetchCACertificatesActionResponse;\n\nexport function ca(state: CAState = initialState, action: PushAction): CAState {\n    switch (action.type) {\n        case actions.CACERT_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case actions.CACERT_FAILURE:\n            return {\n                ...state,\n                loading: false,\n                error: true,\n                errorDetail: action.payload,\n            };\n        case actions.CACERT_SUCCESS:\n            return {\n                ...state,\n                items: action.payload,\n                lastReceived: new Date(),\n                error: false,\n                errorDetail: null,\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/certificates/push_actions.ts",
    "content": "import { RSAA, RSAAction } from 'redux-api-middleware';\nimport {JSONAPI_HEADERS} from '../../store/constants'\nimport {JSONAPIListResponse, JSONAPIDataObject} from \"../json-api\";\nimport {Certificate} from \"../../store/certificates/types\";\nimport {FlaskFilter, FlaskFilters} from \"../../flask-rest-jsonapi\";\n\n\nexport type PUSHCERT_REQUEST = 'certificates/PUSHCERT_REQUEST';\nexport const PUSHCERT_REQUEST: PUSHCERT_REQUEST = 'certificates/PUSHCERT_REQUEST';\nexport type PUSHCERT_SUCCESS = 'certificates/PUSHCERT_SUCCESS';\nexport const PUSHCERT_SUCCESS: PUSHCERT_SUCCESS = 'certificates/PUSHCERT_SUCCESS';\nexport type PUSHCERT_FAILURE = 'certificates/PUSHCERT_FAILURE';\nexport const PUSHCERT_FAILURE: PUSHCERT_FAILURE = 'certificates/PUSHCERT_FAILURE';\n\nexport interface FetchPushCertificatesActionRequest {\n    (): RSAAction<PUSHCERT_REQUEST, PUSHCERT_SUCCESS, PUSHCERT_FAILURE>;\n}\n\nexport interface FetchPushCertificatesActionResponse {\n    type: PUSHCERT_REQUEST | PUSHCERT_SUCCESS | PUSHCERT_FAILURE;\n    payload?: JSONAPIListResponse<JSONAPIDataObject<Certificate>>;\n}\n\nexport const fetchPushCertificates: FetchPushCertificatesActionRequest = (): RSAAction<PUSHCERT_REQUEST, PUSHCERT_SUCCESS, PUSHCERT_FAILURE> => {\n    return {\n        [RSAA]: {\n            endpoint: '/api/v1/push_certificates',\n            method: 'GET',\n            types: [\n                PUSHCERT_REQUEST,\n                PUSHCERT_SUCCESS,\n                PUSHCERT_FAILURE\n            ],\n            headers: JSONAPI_HEADERS\n        }\n    }\n};"
  },
  {
    "path": "ui/src/store/certificates/push_reducer.ts",
    "content": "import {JSONAPIDataObject, JSONAPIListResponse} from \"../json-api\";\nimport * as actions from \"./push_actions\";\nimport {Certificate} from \"./types\";\n\nexport interface PushState {\n    items?: JSONAPIListResponse<JSONAPIDataObject<Certificate>>;\n    loading: boolean;\n    error: boolean;\n    errorDetail?: any\n    lastReceived?: Date;\n}\n\nconst initialState: PushState = {\n    loading: false,\n    error: false,\n    errorDetail: null,\n    lastReceived: null,\n};\n\nexport type PushAction = actions.FetchPushCertificatesActionResponse;\n\nexport function push(state: PushState = initialState, action: PushAction): PushState {\n    switch (action.type) {\n        case actions.PUSHCERT_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case actions.PUSHCERT_FAILURE:\n            return {\n                ...state,\n                loading: false,\n                error: true,\n                errorDetail: action.payload,\n            };\n        case actions.PUSHCERT_SUCCESS:\n            return {\n                ...state,\n                items: action.payload,\n                lastReceived: new Date(),\n                error: false,\n                errorDetail: null,\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/certificates/reducer.ts",
    "content": "import {combineReducers} from \"redux\";\nimport * as actions from \"./actions\";\nimport {\n    DeleteCertificateActionResponse,\n    IndexActionResponse,\n} from \"./actions\";\nimport {FetchPushCertificatesActionResponse} from \"./push_actions\";\n\n// Sub reducers\nimport {JSONAPIDataObject, JSONAPIDetailResponse} from \"../json-api\";\nimport {installed_certificates_reducer, InstalledCertificatesState} from \"../device/installed_certificates_reducer\";\nimport {ca, CAState} from \"./ca_reducer\";\nimport {push, PushState} from \"./push_reducer\";\nimport {ssl, SSLState} from \"./ssl_reducer\";\nimport {Certificate} from \"./types\";\n\nexport interface CertificatesState {\n    items: Array<JSONAPIDataObject<Certificate>>;\n    loading: boolean;\n    error: boolean;\n    errorDetail?: any\n    lastReceived?: Date;\n    currentPage: number;\n    pageSize: number;\n    recordCount?: number;\n    byType?: { [propName: string]: JSONAPIDetailResponse<Certificate, undefined> };\n    push?: PushState;\n    ssl?: SSLState;\n    ca?: CAState;\n}\n\nconst initialState: CertificatesState = {\n    items: [],\n    loading: false,\n    error: false,\n    errorDetail: null,\n    lastReceived: null,\n    currentPage: 1,\n    pageSize: 50,\n    byType: {},\n};\n\ntype CertificatesAction = IndexActionResponse | DeleteCertificateActionResponse;\n\nexport function certificates(state: CertificatesState = initialState, action: CertificatesAction): CertificatesState {\n    switch (action.type) {\n        case actions.INDEX_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n\n        case actions.INDEX_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n            };\n\n        case actions.INDEX_SUCCESS:\n            return {\n                ...state,\n                items: action.payload.data,\n                lastReceived: new Date,\n                loading: false,\n                recordCount: action.payload.meta.count,\n            };\n\n        case actions.DELETE_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n\n        case actions.DELETE_FAILURE:\n            return {\n                ...state,\n                loading: false,\n                error: true,\n                errorDetail: action.payload,\n            };\n\n        case actions.DELETE_SUCCESS:\n            return state;\n\n        default:\n            return {\n                ...state,\n                push: push(state.push, action),\n                ssl: ssl(state.ssl, action),\n                ca: ca(state.ca, action),\n            }\n    }\n}\n"
  },
  {
    "path": "ui/src/store/certificates/ssl_actions.ts",
    "content": "import { RSAA, RSAAction } from 'redux-api-middleware';\nimport {JSONAPI_HEADERS} from '../../store/constants'\nimport {JSONAPIListResponse, JSONAPIDataObject} from \"../json-api\";\nimport {Certificate} from \"./types\";\nimport {FlaskFilter, FlaskFilters} from \"../../flask-rest-jsonapi\";\n\n\nexport type SSLCERT_REQUEST = 'certificates/SSLCERT_REQUEST';\nexport const SSLCERT_REQUEST: SSLCERT_REQUEST = 'certificates/SSLCERT_REQUEST';\nexport type SSLCERT_SUCCESS = 'certificates/SSLCERT_SUCCESS';\nexport const SSLCERT_SUCCESS: SSLCERT_SUCCESS = 'certificates/SSLCERT_SUCCESS';\nexport type SSLCERT_FAILURE = 'certificates/SSLCERT_FAILURE';\nexport const SSLCERT_FAILURE: SSLCERT_FAILURE = 'certificates/SSLCERT_FAILURE';\n\nexport interface FetchSSLCertificatesActionRequest {\n    (): RSAAction<SSLCERT_REQUEST, SSLCERT_SUCCESS, SSLCERT_FAILURE>;\n}\n\nexport interface FetchSSLCertificatesActionResponse {\n    type: SSLCERT_REQUEST | SSLCERT_SUCCESS | SSLCERT_FAILURE;\n    payload?: JSONAPIListResponse<JSONAPIDataObject<Certificate>>;\n}\n\nexport const fetchSSLCertificates: FetchSSLCertificatesActionRequest = (): RSAAction<SSLCERT_REQUEST, SSLCERT_SUCCESS, SSLCERT_FAILURE> => {\n    return {\n        [RSAA]: {\n            endpoint: '/api/v1/ssl_certificates',\n            method: 'GET',\n            types: [\n                SSLCERT_REQUEST,\n                SSLCERT_SUCCESS,\n                SSLCERT_FAILURE\n            ],\n            headers: JSONAPI_HEADERS\n        }\n    }\n};"
  },
  {
    "path": "ui/src/store/certificates/ssl_reducer.ts",
    "content": "import {JSONAPIDataObject, JSONAPIListResponse} from \"../json-api\";\nimport * as actions from \"./ssl_actions\";\nimport {Certificate} from \"./types\";\n\nexport interface SSLState {\n    items?: JSONAPIListResponse<JSONAPIDataObject<Certificate>>;\n    loading: boolean;\n    error: boolean;\n    errorDetail?: any\n    lastReceived?: Date;\n}\n\nconst initialState: SSLState = {\n    error: false,\n    errorDetail: null,\n    lastReceived: null,\n    loading: false,\n};\n\nexport type PushAction = actions.FetchSSLCertificatesActionResponse;\n\nexport function ssl(state: SSLState = initialState, action: PushAction): SSLState {\n    switch (action.type) {\n        case actions.SSLCERT_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case actions.SSLCERT_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n                loading: false,\n            };\n        case actions.SSLCERT_SUCCESS:\n            return {\n                ...state,\n                error: false,\n                errorDetail: null,\n                items: action.payload,\n                lastReceived: new Date(),\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/certificates/types.ts",
    "content": "export interface Certificate {\n    type: string;\n    x509_cn: string;\n    not_before: Date;\n    not_after: Date;\n    fingerprint?: string;\n}\n"
  },
  {
    "path": "ui/src/store/commands/actions.ts",
    "content": "import { RSAA, RSAAction } from \"redux-api-middleware\";\nimport {JSONAPI_HEADERS, JSONAPIDataObject, JSONAPIListResponse} from \"../json-api\";\nimport {Command} from \"../device/types\";\nimport {RootState} from \"../../reducers\";\n\nexport type INDEX_REQUEST = \"commands/INDEX_REQUEST\";\nexport const INDEX_REQUEST: INDEX_REQUEST = \"commands/INDEX_REQUEST\";\nexport type INDEX_SUCCESS = \"commands/INDEX_SUCCESS\";\nexport const INDEX_SUCCESS: INDEX_SUCCESS = \"commands/INDEX_SUCCESS\";\nexport type INDEX_FAILURE = \"commands/INDEX_FAILURE\";\nexport const INDEX_FAILURE: INDEX_FAILURE = \"commands/INDEX_FAILURE\";\n\nexport type POST_REQUEST = \"commands/POST_REQUEST\";\nexport const POST_REQUEST: POST_REQUEST = \"commands/POST_REQUEST\";\nexport type POST_SUCCESS = \"commands/POST_SUCCESS\";\nexport const POST_SUCCESS: POST_SUCCESS = \"commands/POST_SUCCESS\";\nexport type POST_FAILURE = \"commands/POST_FAILURE\";\nexport const POST_FAILURE: POST_FAILURE = \"commands/POST_FAILURE\";\n\ntype PostActionRequest = (values: Command) => RSAAction<POST_REQUEST, POST_SUCCESS, POST_FAILURE>;\n\nexport interface PostActionResponse {\n    type: POST_REQUEST | POST_FAILURE | POST_SUCCESS;\n    payload?: JSONAPIListResponse<JSONAPIDataObject<Command>>;\n}\n\nexport const post: PostActionRequest = (values: Command, device_id?: number) => {\n\n    let endpoint = \"/api/v1/commands\";\n\n    if (device_id) {\n        endpoint = `/api/v1/devices/${device_id}/commands`;\n    }\n\n    return {\n        [RSAA]: {\n            body: JSON.stringify({\n                data: {\n                    attributes: values,\n                    type: \"commands\",\n                },\n            }),\n            endpoint,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                POST_REQUEST,\n                POST_SUCCESS,\n                POST_FAILURE,\n            ],\n        },\n    }\n};\n"
  },
  {
    "path": "ui/src/store/commands/reducer.ts",
    "content": "import * as actions from './actions';\nimport {PostActionResponse} from \"./actions\";\n\nexport interface CommandsState {\n    \n}\n\nconst initialState: CommandsState = {\n    \n};\n\ntype CommandAction = PostActionResponse;\n\nexport function commands (state: CommandsState = initialState, action: CommandAction): CommandsState {\n    switch (action.type) {\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/configuration/apns_reducer.ts",
    "content": "import {ICsrActionResponse, MDMCertActionTypes, UploadCryptedActionResponse} from \"./mdmcert_actions\";\nimport {IMDMCertResponse} from \"./types\";\nimport {isApiError} from \"../../guards\";\nimport {ApiError} from \"redux-api-middleware\";\n\nexport interface APNSState {\n    csrError: ApiError<any>;\n    csrLoading: boolean;\n    csrResult: IMDMCertResponse;\n    data?: any;\n    decryptError: ApiError<any>;\n    decryptLoading: boolean;\n    registeredEmail: string;\n}\n\nconst initialState: APNSState = {\n    csrError: null,\n    csrLoading: false,\n    csrResult: null,\n    decryptError: null,\n    decryptLoading: false,\n    registeredEmail: \"\",\n};\n\ntype APNSAction = ICsrActionResponse | UploadCryptedActionResponse;\n\nexport function apns(state: APNSState = initialState, action: APNSAction): APNSState {\n    switch (action.type) {\n        case MDMCertActionTypes.MDMCERT_CSR_REQUEST:\n            return {\n                ...state,\n                csrError: null,\n                csrLoading: true,\n                csrResult: null,\n            };\n        case MDMCertActionTypes.MDMCERT_CSR_SUCCESS:\n            if (isApiError(action.payload)) {\n                return {\n                    ...state,\n                    csrError: action.payload,\n                    csrLoading: false,\n                }\n            } else {\n                return {\n                    ...state,\n                    csrError: null,\n                    csrLoading: false,\n                    csrResult: action.payload,\n                };\n            }\n        case MDMCertActionTypes.MDMCERT_CSR_FAILURE:\n            return {\n                ...state,\n                csrError: action.payload,\n                csrLoading: false,\n            };\n        case MDMCertActionTypes.UPLOAD_CRYPTED_REQUEST:\n            return {\n                ...state,\n                decryptError: null,\n                decryptLoading: true,\n            };\n        case MDMCertActionTypes.UPLOAD_CRYPTED_FAILURE:\n            return {\n                ...state,\n                decryptError: action.payload,\n                decryptLoading: false,\n            };\n        case MDMCertActionTypes.UPLOAD_CRYPTED_SUCCESS:\n            return {\n                ...state,\n                decryptError: null,\n                decryptLoading: false,\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/configuration/mdmcert_actions.ts",
    "content": "import {Action, Dispatch} from \"redux\";\nimport {ApiError, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {ThunkAction} from \"redux-thunk\";\nimport {RootState} from \"../../reducers\";\nimport {JSON_HEADERS} from \"../constants\";\nimport {JSONAPIDetailResponse, RSAAReadActionRequest, RSAAReadActionResponse} from \"../json-api\";\nimport {IMDMCertResponse} from \"./types\";\n\nexport enum MDMCertActionTypes {\n     MDMCERT_CSR_REQUEST = \"mdmcert/CSR_REQUEST\",\n     MDMCERT_CSR_SUCCESS = \"mdmcert/CSR_SUCCESS\",\n     MDMCERT_CSR_FAILURE = \"mdmcert/CSR_FAILURE\",\n     UPLOAD_CRYPTED_REQUEST = \"mdmcert/UPLOAD_CRYPTED_REQUEST\",\n     UPLOAD_CRYPTED_SUCCESS = \"mdmcert/UPLOAD_CRYPTED_SUCCESS\",\n     UPLOAD_CRYPTED_FAILURE = \"mdmcert/UPLOAD_CRYPTED_FAILURE\",\n}\n\nexport type CsrActionRequest = (email: string) => RSAAction<\n    MDMCertActionTypes.MDMCERT_CSR_REQUEST,\n    MDMCertActionTypes.MDMCERT_CSR_SUCCESS,\n    MDMCertActionTypes.MDMCERT_CSR_FAILURE>;\n\nexport interface ICsrActionResponse {\n    type: MDMCertActionTypes.MDMCERT_CSR_REQUEST |\n          MDMCertActionTypes.MDMCERT_CSR_SUCCESS |\n          MDMCertActionTypes.MDMCERT_CSR_FAILURE;\n    payload?: ApiError | IMDMCertResponse;\n}\n\nexport const csr: CsrActionRequest = (email: string) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/mdmcert/request/${email}`,\n            headers: JSON_HEADERS,\n            method: \"GET\",\n            types: [\n                MDMCertActionTypes.MDMCERT_CSR_REQUEST,\n                MDMCertActionTypes.MDMCERT_CSR_SUCCESS,\n                MDMCertActionTypes.MDMCERT_CSR_FAILURE,\n            ],\n        },\n    };\n};\n\nexport type UploadCryptedActionRequest = (file: File) => ThunkAction<void, RootState, void, UploadCryptedActionResponse>;\nexport type UploadCryptedActionResponse = RSAAReadActionResponse<\n    MDMCertActionTypes.UPLOAD_CRYPTED_REQUEST,\n    MDMCertActionTypes.UPLOAD_CRYPTED_SUCCESS,\n    MDMCertActionTypes.UPLOAD_CRYPTED_FAILURE,\n    JSONAPIDetailResponse<any, undefined>>;\n\nexport const uploadCrypted: UploadCryptedActionRequest = (file) => (\n    dispatch: Dispatch,\n    getState: () => RootState,\n    extraArgument: void) => {\n\n    const data = new FormData();\n    data.append(\"file\", file);\n    // dispatch({\n    //     payload: data,\n    //     type: UPLOAD_TOKEN,\n    // });\n\n    dispatch({\n        [RSAA]: {\n            body: data,\n            endpoint: `/api/v1/mdmcert/decrypt`,\n            method: \"POST\",\n            types: [\n                MDMCertActionTypes.UPLOAD_CRYPTED_REQUEST,\n                MDMCertActionTypes.UPLOAD_CRYPTED_SUCCESS,\n                MDMCertActionTypes.UPLOAD_CRYPTED_FAILURE,\n            ],\n        },\n    });\n};\n"
  },
  {
    "path": "ui/src/store/configuration/reducer.ts",
    "content": "import {combineReducers} from \"redux\";\n\nimport {apns, APNSState} from \"./apns_reducer\";\nimport {scep, SCEPState} from \"./scep_reducer\";\nimport {vpp, VPPState} from \"./vpp_reducer\";\n\nexport interface ConfigurationState {\n    scep: SCEPState;\n    vpp: VPPState;\n    apns: APNSState;\n}\n\nconst initialState: ConfigurationState = {\n    apns: null,\n    scep: null,\n    vpp: null,\n};\n\nexport function configuration(state: ConfigurationState = initialState, action: any): ConfigurationState {\n    return combineReducers({\n        apns,\n        scep,\n        vpp,\n    })(state, action);\n}\n"
  },
  {
    "path": "ui/src/store/configuration/scep_actions.ts",
    "content": "import { RSAA, RSAAction } from 'redux-api-middleware';\nimport {JSONAPI_HEADERS, JSON_HEADERS} from '../constants'\nimport {SCEPConfiguration} from \"./types\";\n\nexport type READ_REQUEST = 'scep/READ_REQUEST';\nexport const READ_REQUEST: READ_REQUEST = 'scep/READ_REQUEST';\nexport type READ_SUCCESS = 'scep/READ_SUCCESS';\nexport const READ_SUCCESS: READ_SUCCESS = 'scep/READ_SUCCESS';\nexport type READ_FAILURE = 'scep/READ_FAILURE';\nexport const READ_FAILURE: READ_FAILURE = 'scep/READ_FAILURE';\n\nexport interface ReadActionRequest {\n    (): RSAAction<READ_REQUEST, READ_SUCCESS, READ_FAILURE>;\n}\n\nexport interface ReadActionResponse {\n    type: READ_REQUEST | READ_SUCCESS | READ_FAILURE;\n    payload?: SCEPConfiguration;\n}\n\nexport const read: ReadActionRequest = () => {\n    return {\n        [RSAA]: {\n            endpoint: '/api/v1/configuration/scep',\n            method: 'GET',\n            types: [\n                READ_REQUEST,\n                READ_SUCCESS,\n                READ_FAILURE\n            ],\n            headers: JSON_HEADERS\n        }\n    }\n};\n\nexport type POST_REQUEST = 'scep/POST_REQUEST';\nexport const POST_REQUEST: POST_REQUEST = 'scep/POST_REQUEST';\nexport type POST_SUCCESS = 'scep/POST_SUCCESS';\nexport const POST_SUCCESS: POST_SUCCESS = 'scep/POST_SUCCESS';\nexport type POST_FAILURE = 'scep/POST_FAILURE';\nexport const POST_FAILURE: POST_FAILURE = 'scep/POST_FAILURE';\n\nexport interface PostActionRequest {\n    (values: SCEPConfiguration): RSAAction<POST_REQUEST, POST_SUCCESS, POST_FAILURE>;\n}\n\nexport interface PostActionResponse {\n    type: POST_REQUEST | POST_FAILURE | POST_SUCCESS;\n    payload?: SCEPConfiguration;\n}\n\nexport const post: PostActionRequest = (values: SCEPConfiguration) => {\n    return {\n        [RSAA]: {\n            endpoint: '/api/v1/configuration/scep',\n            method: 'POST',\n            types: [\n                POST_REQUEST,\n                POST_SUCCESS,\n                POST_FAILURE\n            ],\n            headers: JSON_HEADERS,\n            body: JSON.stringify(values)\n        }\n    }\n};\n"
  },
  {
    "path": "ui/src/store/configuration/scep_reducer.ts",
    "content": "import {PostActionResponse, ReadActionResponse} from \"./scep_actions\";\nimport * as actions from \"./scep_actions\";\nimport {SCEPConfiguration} from \"./types\";\n\nexport interface SCEPState {\n    data?: SCEPConfiguration;\n    loading: boolean;\n    submitted: boolean;\n    error: boolean;\n    errorDetail?: any;\n}\n\nconst initialState: SCEPState = {\n    loading: false,\n    error: false,\n    submitted: false,\n};\n\ntype SCEPAction = ReadActionResponse | PostActionResponse;\n\nexport function scep(state: SCEPState = initialState, action: SCEPAction): SCEPState {\n    switch (action.type) {\n        case actions.READ_SUCCESS:\n            return {\n                ...state,\n                data: action.payload,\n                loading: false,\n            };\n        case actions.READ_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case actions.READ_FAILURE:\n            return {\n                ...state,\n                error: true,\n                loading: false,\n            };\n        case actions.POST_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case actions.POST_FAILURE:\n            return {\n                ...state,\n                error: true,\n                loading: false,\n            };\n        case actions.POST_SUCCESS:\n            return {\n                ...state,\n                error: false,\n                loading: false,\n                submitted: true,\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/configuration/types.ts",
    "content": "export interface SCEPConfiguration {\n    url: string;\n    challenge_enabled: boolean;\n    challenge: string;\n    ca_fingerprint: string;\n    subject: string;\n    key_size: string; // Needs to be string to support redux-form\n    key_type: \"RSA\";\n    key_usage: string;\n    subject_alt_name: string;\n    retries: number;\n    retry_delay: number;\n    certificate_renewal_time_interval: number;\n}\n\nexport interface VPPAccount {\n    org_name: string;\n    exp_date: string;\n}\n\n// Possible Responses from mdmcert.download\nexport interface IMDMCertResponse {\n    result: \"failure\" | \"success\";\n    reason?: string;\n}\n"
  },
  {
    "path": "ui/src/store/configuration/vpp.ts",
    "content": "import {Dispatch} from \"redux\";\nimport {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {ThunkAction} from \"redux-thunk\";\nimport {\n    JSONAPIDetailResponse, RSAAIndexActionRequest,\n    RSAAIndexActionResponse,\n    RSAAReadActionRequest,\n    RSAAReadActionResponse,\n} from \"../json-api\";\nimport {RootState} from \"../../reducers/index\";\nimport {JSON_HEADERS, JSONAPI_HEADERS} from \"../constants\";\nimport {VPPAccount} from \"./types\";\nimport {encodeJSONAPIIndexParameters} from \"../../flask-rest-jsonapi\";\n\nexport enum VPPActionTypes {\n    TOKEN_REQUEST = \"vpp/TOKEN_REQUEST\",\n    TOKEN_SUCCESS = \"vpp/TOKEN_SUCCESS\",\n    TOKEN_FAILURE = \"vpp/TOKEN_FAILURE\",\n    UPLOAD_REQUEST = \"vpp/UPLOAD_REQUEST\",\n    UPLOAD_SUCCESS = \"vpp/UPLOAD_SUCCESS\",\n    UPLOAD_FAILURE = \"vpp/UPLOAD_FAILURE\",\n    UPLOAD_TOKEN = \"vpp/UPLOAD_TOKEN\",\n    INDEX_REQUEST = \"vpp/INDEX_REQUEST\",\n    INDEX_SUCCESS = \"vpp/INDEX_SUCCESS\",\n    INDEX_FAILURE = \"vpp/INDEX_FAILURE\",\n}\n\nexport interface IVPPAction {\n    type: VPPActionTypes;\n}\n\nexport type TokenActionRequest = RSAAReadActionRequest<VPPActionTypes.TOKEN_REQUEST, VPPActionTypes.TOKEN_SUCCESS, VPPActionTypes.TOKEN_FAILURE>;\nexport type TokenActionResponse = RSAAReadActionResponse<VPPActionTypes.TOKEN_REQUEST, VPPActionTypes.TOKEN_SUCCESS, VPPActionTypes.TOKEN_FAILURE, JSONAPIDetailResponse<VPPAccount, undefined>>;\n\nexport const read: TokenActionRequest = (id: string) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/vpp/token\",\n            headers: JSON_HEADERS,\n            method: \"GET\",\n            types: [\n                VPPActionTypes.TOKEN_REQUEST,\n                VPPActionTypes.TOKEN_SUCCESS,\n                VPPActionTypes.TOKEN_FAILURE,\n            ],\n        },\n    } as RSAAction<VPPActionTypes.TOKEN_REQUEST, VPPActionTypes.TOKEN_SUCCESS, VPPActionTypes.TOKEN_FAILURE>);\n};\n\nexport type UploadActionRequest = (file: File) => ThunkAction<void, RootState, void, UploadActionResponse>;\nexport type UploadActionResponse = RSAAReadActionResponse<VPPActionTypes.UPLOAD_REQUEST, VPPActionTypes.UPLOAD_SUCCESS, VPPActionTypes.UPLOAD_FAILURE,\n    JSONAPIDetailResponse<VPPAccount, undefined>>;\n\nexport const upload = (file: File): ThunkAction<void, RootState, void, UploadActionResponse> => (\n    dispatch: Dispatch,\n    getState: () => RootState,\n    extraArgument: void) => {\n\n    const data = new FormData();\n    data.append(\"file\", file);\n    dispatch({\n        payload: data,\n        type: VPPActionTypes.UPLOAD_TOKEN,\n    });\n\n    dispatch({\n        [RSAA]: {\n            body: data,\n            endpoint: `/api/v1/vpp/upload/token`,\n            method: \"POST\",\n            types: [\n                VPPActionTypes.UPLOAD_REQUEST,\n                VPPActionTypes.UPLOAD_SUCCESS,\n                VPPActionTypes.UPLOAD_FAILURE,\n            ],\n        },\n    });\n};\n\nexport type IndexActionRequest = RSAAIndexActionRequest<VPPActionTypes.INDEX_REQUEST, VPPActionTypes.INDEX_SUCCESS, VPPActionTypes.INDEX_FAILURE>;\nexport type IndexActionResponse = RSAAIndexActionResponse<VPPActionTypes.INDEX_REQUEST, VPPActionTypes.INDEX_SUCCESS, VPPActionTypes.INDEX_FAILURE, VPPAccount>;\n\nexport const index = encodeJSONAPIIndexParameters((queryParameters: string[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/vpp_accounts?\" + queryParameters.join(\"&\"),\n            headers: JSONAPI_HEADERS,\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                VPPActionTypes.INDEX_REQUEST,\n                VPPActionTypes.INDEX_SUCCESS,\n                VPPActionTypes.INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<VPPActionTypes.INDEX_REQUEST, VPPActionTypes.INDEX_SUCCESS, VPPActionTypes.INDEX_FAILURE>);\n});\n"
  },
  {
    "path": "ui/src/store/configuration/vpp_reducer.ts",
    "content": "import {isJSONAPIErrorResponsePayload, JSONAPIDetailResponse} from \"../json-api\";\nimport {VPPAccount} from \"./types\";\nimport {TokenActionResponse, VPPActionTypes} from \"./vpp\";\n\nexport interface VPPState {\n    data?: JSONAPIDetailResponse<VPPAccount, void>;\n    loading: boolean;\n    submitted: boolean;\n    error: boolean;\n    errorDetail?: any;\n}\n\nconst initialState: VPPState = {\n    error: false,\n    loading: false,\n    submitted: false,\n};\n\ntype VPPAction = TokenActionResponse;\n\nexport function vpp(state: VPPState = initialState, action: VPPAction): VPPState {\n    switch (action.type) {\n        case VPPActionTypes.TOKEN_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case VPPActionTypes.TOKEN_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return state;\n            } else {\n                return {\n                    ...state,\n                    data: action.payload,\n                    loading: false,\n                };\n            }\n        case VPPActionTypes.TOKEN_FAILURE:\n            return {\n                ...state,\n                error: true,\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/configureStore.ts",
    "content": "import {applyMiddleware, compose, createStore, Store} from \"redux\";\nimport {Middleware} from \"redux\";\nimport {apiMiddleware} from \"redux-api-middleware\";\nimport thunk from \"redux-thunk\";\nimport rootReducer from \"../reducers\";\nimport {RootState} from \"../reducers\";\nimport {createBrowserHistory} from \"history\";\n\nconst composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;\n\nexport const history = createBrowserHistory();\n\nexport const configureStore = (initialState: RootState, ...middlewares: Middleware[] ): Store<any> => {\n\n    const enhancer = composeEnhancers(\n        applyMiddleware(\n            thunk,\n            apiMiddleware,\n            ...middlewares,\n        ),\n    );\n\n    const store = createStore(\n        rootReducer(history),\n        initialState,\n        enhancer,\n    );\n\n    if (module.hot) {\n        module.hot.accept(\"../reducers\", () => {\n            const nextRootReducer = require(\"../reducers\").default;\n            store.replaceReducer(nextRootReducer)\n        });\n    }\n\n    return store;\n};\n\nexport default configureStore;\n"
  },
  {
    "path": "ui/src/store/constants.ts",
    "content": "export const JSONAPI_HEADERS = {\n    \"Accept\": \"application/vnd.api+json\",\n    \"Content-Type\": \"application/vnd.api+json\",\n};\n\nexport const JSON_HEADERS = {\n    \"Accept\": \"application/json\",\n    \"Content-Type\": \"application/json\",\n};\n\n// TODO: This is for resource owner password grant but we should use something much more secure.\nexport const OAUTH2_CLIENT_ID = \"F8955645-A21D-44AE-9387-42B0800ADF15\";\nexport const OAUTH2_CLIENT_SECRET = \"A\";\n\n// Flask-REST-JSONAPI Filter and Sort definitions\n\nexport interface OtherAction {\n    type: string;\n    payload?: any;\n}\n"
  },
  {
    "path": "ui/src/store/dep/account_reducer.ts",
    "content": "import {Reducer} from \"redux\";\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\nimport {DEPActions, DEPActionTypes} from \"./actions\";\nimport {DEPAccount, DEPProfile} from \"./types\";\nimport {isApiError} from \"../../guards\";\n\nexport interface IDEPAccountState {\n    readonly dep_account?: JSONAPIDataObject<DEPAccount>;\n    readonly dep_profiles?: Array<JSONAPIDataObject<DEPProfile>>;\n    readonly loading: boolean;\n    readonly error: boolean;\n    readonly errorDetail?: any;\n}\n\nconst initialState: IDEPAccountState = {\n    error: false,\n    loading: false,\n};\n\nexport const account: Reducer<IDEPAccountState, DEPActions> = (state = initialState, action) => {\n    switch (action.type) {\n\n        case DEPActionTypes.ACCT_READ_REQUEST:\n            return { ...state, loading: true };\n        case DEPActionTypes.ACCT_READ_SUCCESS:\n            const payload = action.payload;\n\n            if (isJSONAPIErrorResponsePayload(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: payload,\n                    loading: false,\n                };\n            } else if (isApiError(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: payload,\n                    loading: false,\n                }\n            } else {\n                return {...state,\n                    dep_account: payload.data,\n                    dep_profiles: payload.included,\n                    loading: false,\n                };\n            }\n        case DEPActionTypes.ACCT_READ_FAILURE:\n            return { ...state, loading: false, error: true, errorDetail: action.payload };\n\n        default:\n            return state;\n    }\n};\n"
  },
  {
    "path": "ui/src/store/dep/accounts_reducer.ts",
    "content": "import {DEPAccount} from \"./types\";\nimport {DEPActions, DEPActionTypes} from \"./actions\";\n\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\n\nexport interface IDEPAccountsState {\n    data?: Array<JSONAPIDataObject<DEPAccount>>;\n    loading: boolean;\n    submitted: boolean;\n    error: boolean;\n    errorDetail?: any;\n}\n\nconst initialState: IDEPAccountsState = {\n    error: false,\n    loading: false,\n    submitted: false,\n};\n\n// type VPPAction = ITokenActionResponse;\n\nexport function accounts(state: IDEPAccountsState = initialState, action: DEPActions): IDEPAccountsState {\n    switch (action.type) {\n        case DEPActionTypes.ACCT_INDEX_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case DEPActionTypes.ACCT_INDEX_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                    loading: false,\n                }\n            } else {\n                return {\n                    ...state,\n                    data: action.payload.data,\n                    loading: false,\n                };\n            }\n        case DEPActionTypes.ACCT_INDEX_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n                loading: false,\n            };\n\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/dep/actions.ts",
    "content": "import {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {POST_FAILURE, POST_REQUEST, POST_SUCCESS} from \"../commands/actions\";\nimport {JSONAPI_HEADERS} from \"../constants\";\nimport {\n    JSONAPIDataObject,\n    JSONAPIDetailResponse,\n    JSONAPIRelationships,\n    JSONAPIResourceIdentifier,\n    RSAAIndexActionRequest,\n    RSAAIndexActionResponse,\n    RSAAPatchActionRequest,\n    RSAAPostActionRequest,\n    RSAAPostActionResponse,\n    RSAAReadActionRequest,\n    RSAAReadActionResponse,\n} from \"../json-api\";\nimport {DEPAccount, DEPProfile} from \"./types\";\nimport {IDEPProfileFormValues} from \"../../components/forms/DEPProfileForm\";\nimport {encodeJSONAPIChildIndexParameters, encodeJSONAPIIndexParameters} from \"../../flask-rest-jsonapi\";\nimport {Relationships} from \"../../json-api-v1\";\nimport {\n    RSAAActionResponse,\n    RSAAIndexActionCreator, RSAAPatchActionCreator,\n    RSAAPostActionCreator,\n    RSAAReadActionCreator\n} from \"../redux-api-middleware\";\nimport {RootState} from \"../../reducers\";\n\nexport enum DEPActionTypes {\n    ACCT_INDEX_REQUEST = \"dep/account/INDEX_REQUEST\",\n    ACCT_INDEX_SUCCESS = \"dep/account/INDEX_SUCCESS\",\n    ACCT_INDEX_FAILURE = \"dep/account/INDEX_FAILURE\",\n\n    ACCT_READ_REQUEST = \"dep/account/READ_REQUEST\",\n    ACCT_READ_SUCCESS = \"dep/account/READ_SUCCESS\",\n    ACCT_READ_FAILURE = \"dep/account/READ_FAILURE\",\n\n    PROF_INDEX_REQUEST = \"dep/profile/INDEX_REQUEST\",\n    PROF_INDEX_SUCCESS = \"dep/profile/INDEX_SUCCESS\",\n    PROF_INDEX_FAILURE = \"dep/profile/INDEX_FAILURE\",\n\n    PROF_READ_REQUEST = \"dep/profile/READ_REQUEST\",\n    PROF_READ_SUCCESS = \"dep/profile/READ_SUCCESS\",\n    PROF_READ_FAILURE = \"dep/profile/READ_FAILURE\",\n\n    PROF_POST_REQUEST = \"dep/profile/POST_REQUEST\",\n    PROF_POST_SUCCESS = \"dep/profile/POST_SUCCESS\",\n    PROF_POST_FAILURE = \"dep/profile/POST_FAILURE\",\n\n    PROF_PATCH_REQUEST = \"dep/profile/PATCH_REQUEST\",\n    PROF_PATCH_SUCCESS = \"dep/profile/PATCH_SUCCESS\",\n    PROF_PATCH_FAILURE = \"dep/profile/PATCH_FAILURE\",\n}\n\n// export type AccountIndexActionRequest = RSAAIndexActionRequest<\n//     DEPActionTypes.ACCT_INDEX_REQUEST,\n//     DEPActionTypes.ACCT_INDEX_SUCCESS,\n//     DEPActionTypes.ACCT_INDEX_FAILURE>;\n// export type AccountIndexActionResponse = RSAAIndexActionResponse<\n//     DEPActionTypes.ACCT_INDEX_REQUEST,\n//     DEPActionTypes.ACCT_INDEX_SUCCESS,\n//     DEPActionTypes.ACCT_INDEX_FAILURE,\n//     DEPAccount>;\n\nexport type AccountIndexActionResponse = RSAAActionResponse<\n    DEPActionTypes.ACCT_INDEX_REQUEST,\n    DEPActionTypes.ACCT_INDEX_SUCCESS,\n    DEPActionTypes.ACCT_INDEX_FAILURE,\n    DEPAccount,\n    void>;\n\nexport type AccountIndexActionCreator = RSAAIndexActionCreator<\n    DEPActionTypes.ACCT_INDEX_REQUEST,\n    DEPActionTypes.ACCT_INDEX_SUCCESS,\n    DEPActionTypes.ACCT_INDEX_FAILURE>;\n\nexport const accounts: AccountIndexActionCreator = encodeJSONAPIIndexParameters((queryParameters: string[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/dep/accounts/?\" + queryParameters.join(\"&\"),\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                DEPActionTypes.ACCT_INDEX_REQUEST,\n                DEPActionTypes.ACCT_INDEX_SUCCESS,\n                DEPActionTypes.ACCT_INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<DEPActionTypes.ACCT_INDEX_REQUEST, DEPActionTypes.ACCT_INDEX_SUCCESS, DEPActionTypes.ACCT_INDEX_FAILURE>);\n});\n\nexport type AccountReadActionCreator = RSAAReadActionCreator<\n    DEPActionTypes.ACCT_READ_REQUEST,\n    DEPActionTypes.ACCT_READ_SUCCESS,\n    DEPActionTypes.ACCT_READ_FAILURE>;\n\nexport type AccountReadActionResponse = RSAAActionResponse<\n    DEPActionTypes.ACCT_READ_REQUEST,\n    DEPActionTypes.ACCT_READ_SUCCESS,\n    DEPActionTypes.ACCT_READ_FAILURE,\n    DEPAccount,\n    void>;\n\nexport const account: AccountReadActionCreator = (id: string, include?: string[]) => {\n\n    let inclusions = \"\";\n    if (include && include.length) {\n        inclusions = \"include=\" + include.join(\",\");\n    }\n\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/dep/accounts/${id}?${inclusions}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                DEPActionTypes.ACCT_READ_REQUEST,\n                DEPActionTypes.ACCT_READ_SUCCESS,\n                DEPActionTypes.ACCT_READ_FAILURE,\n            ],\n        },\n    };\n};\n\nexport type ProfileIndexActionCreator = RSAAIndexActionCreator<\n    DEPActionTypes.PROF_INDEX_REQUEST,\n    DEPActionTypes.PROF_INDEX_SUCCESS,\n    DEPActionTypes.PROF_INDEX_FAILURE>;\n\nexport type ProfileIndexActionResponse = RSAAActionResponse<\n    DEPActionTypes.PROF_INDEX_REQUEST,\n    DEPActionTypes.PROF_INDEX_SUCCESS,\n    DEPActionTypes.PROF_INDEX_FAILURE,\n    DEPProfile,\n    void>;\n\nexport const profiles = encodeJSONAPIChildIndexParameters((dep_account_id: string, queryParameters: string[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: `/api/v1/dep/accounts/${dep_account_id}/profiles?` + queryParameters.join(\"&\"),\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                DEPActionTypes.PROF_INDEX_REQUEST,\n                DEPActionTypes.PROF_INDEX_SUCCESS,\n                DEPActionTypes.PROF_INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<DEPActionTypes.PROF_INDEX_REQUEST, DEPActionTypes.PROF_INDEX_SUCCESS, DEPActionTypes.PROF_INDEX_FAILURE>);\n});\n\nexport type ProfileReadActionCreator = RSAAReadActionCreator<\n    DEPActionTypes.PROF_READ_REQUEST,\n    DEPActionTypes.PROF_READ_SUCCESS,\n    DEPActionTypes.PROF_READ_FAILURE>;\n\nexport type ProfileReadActionResponse = RSAAActionResponse<\n    DEPActionTypes.PROF_READ_REQUEST,\n    DEPActionTypes.PROF_READ_SUCCESS,\n    DEPActionTypes.PROF_READ_FAILURE,\n    DEPProfile,\n    void>;\n\nexport const profile: ProfileReadActionCreator = (id: string, include?: string[]) => {\n\n    let inclusions = \"\";\n    if (include && include.length) {\n        inclusions = \"include=\" + include.join(\",\");\n    }\n\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/dep/profiles/${id}?${inclusions}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                DEPActionTypes.PROF_READ_REQUEST,\n                DEPActionTypes.PROF_READ_SUCCESS,\n                DEPActionTypes.PROF_READ_FAILURE,\n            ],\n        },\n    };\n};\n\nexport type ProfilePostActionCreator = RSAAPostActionCreator<\n    DEPActionTypes.PROF_POST_REQUEST,\n    DEPActionTypes.PROF_POST_SUCCESS,\n    DEPActionTypes.PROF_POST_FAILURE,\n    DEPProfile>;\n\nexport type ProfilePostActionResponse = RSAAActionResponse<\n    DEPActionTypes.PROF_POST_REQUEST,\n    DEPActionTypes.PROF_POST_SUCCESS,\n    DEPActionTypes.PROF_POST_FAILURE,\n    DEPProfile,\n    void>;\n\nexport const postProfile: ProfilePostActionCreator =\n    (values: DEPProfile, relationships: Relationships) => {\n\n    const bodyData: JSONAPIDetailResponse<DEPProfile, void> = {\n        data: {\n            attributes: values,\n            type: \"dep_profiles\",\n        },\n    };\n\n    if (relationships) {\n        const relationshipData: JSONAPIRelationships = {};\n        for (const k in relationships) {\n            if (relationships.hasOwnProperty(k)) {\n                relationshipData[k] = { data: relationships[k] };\n            }\n        }\n\n        bodyData.data.relationships = relationshipData;\n    }\n\n    return {\n        [RSAA]: {\n            body: JSON.stringify(bodyData),\n            endpoint: `/api/v1/dep/profiles/`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                DEPActionTypes.PROF_POST_REQUEST,\n                DEPActionTypes.PROF_POST_SUCCESS,\n                DEPActionTypes.PROF_POST_FAILURE,\n            ],\n        },\n    };\n\n};\n\nexport type ProfilePatchActionRequest = RSAAPatchActionCreator<\n    DEPActionTypes.PROF_PATCH_REQUEST,\n    DEPActionTypes.PROF_PATCH_SUCCESS,\n    DEPActionTypes.PROF_PATCH_FAILURE,\n    DEPProfile>;\n\nexport const patchProfile: ProfilePatchActionRequest = (id: string, values: DEPProfile) => {\n    const bodyData = {\n        data: {\n            attributes: values,\n            id,\n            type: \"dep_profiles\",\n        },\n    };\n\n    return {\n        [RSAA]: {\n            body: JSON.stringify(bodyData),\n            endpoint: `/api/v1/dep/profiles/${id}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"PATCH\",\n            types: [\n                DEPActionTypes.PROF_PATCH_REQUEST,\n                DEPActionTypes.PROF_PATCH_SUCCESS,\n                DEPActionTypes.PROF_PATCH_FAILURE,\n            ],\n        },\n    };\n};\n\nexport type DEPActions = AccountIndexActionResponse &\n    AccountReadActionResponse &\n    ProfileIndexActionResponse &\n    ProfileReadActionResponse &\n    ProfilePostActionResponse;\n"
  },
  {
    "path": "ui/src/store/dep/profile_reducer.ts",
    "content": "import {Reducer} from \"redux\";\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject, RSAAResponseSuccess} from \"../json-api\";\nimport {DEPActions, DEPActionTypes} from \"./actions\";\nimport {DEPProfile} from \"./types\";\nimport {isApiError} from \"../../guards\";\n\nexport interface IDEPProfileState {\n    readonly dep_profile?: JSONAPIDataObject<DEPProfile>;\n    readonly loading: boolean;\n    readonly error: boolean;\n    readonly errorDetail?: any;\n}\n\nconst initialState: IDEPProfileState = {\n    error: false,\n    loading: false,\n};\n\nexport const profile: Reducer<IDEPProfileState, DEPActions> = (state = initialState, action) => {\n    switch (action.type) {\n\n        case DEPActionTypes.PROF_READ_REQUEST:\n            return {\n                ...state,\n                loading: true,\n                error: false,\n                errorDetail: null,\n                dep_profile: null,\n            };\n        case DEPActionTypes.PROF_READ_SUCCESS:\n            let payload = action.payload;\n            if (isApiError(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: payload,\n                    loading: false,\n                }\n            } else if (isJSONAPIErrorResponsePayload(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                    loading: false,\n                };\n            } else {\n                return {\n                    ...state,\n                    dep_profile: payload.data,\n                    loading: false,\n                };\n            }\n        case DEPActionTypes.PROF_READ_FAILURE:\n            return { ...state, loading: false, error: true, errorDetail: action.payload };\n\n        case DEPActionTypes.PROF_POST_REQUEST:\n            return { ...state, loading: true };\n        case DEPActionTypes.PROF_POST_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                    loading: false,\n                };\n            } else {\n                return {\n                    ...state,\n                    dep_account: action.payload.data,\n                    loading: false,\n                };\n            }\n        case DEPActionTypes.PROF_POST_FAILURE:\n            return { ...state, loading: false, error: true, errorDetail: action.payload };\n\n        case DEPActionTypes.PROF_PATCH_REQUEST:\n            return { ...state, loading: true };\n        case DEPActionTypes.PROF_PATCH_SUCCESS:\n            const payload = action.payload;\n            if (isJSONAPIErrorResponsePayload(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: payload,\n                    loading: false,\n                };\n            } else {\n                return {\n                    ...state,\n                    dep_account: payload.data,\n                    loading: false,\n                };\n            }\n        case DEPActionTypes.PROF_PATCH_FAILURE:\n            return { ...state, loading: false, error: true, errorDetail: action.payload };\n\n        default:\n            return state;\n    }\n};\n"
  },
  {
    "path": "ui/src/store/dep/profiles_reducer.ts",
    "content": "import {DEPActions, DEPActionTypes} from \"./actions\";\nimport {DEPProfile} from \"./types\";\n\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\n\nexport interface IDEPProfilesState {\n    data?: Array<JSONAPIDataObject<DEPProfile>>;\n    error: boolean;\n    errorDetail?: any;\n    loading: boolean;\n    submitted: boolean;\n}\n\nconst initialState: IDEPProfilesState = {\n    error: false,\n    loading: false,\n    submitted: false,\n};\n\n// type VPPAction = ITokenActionResponse;\n\nexport function profiles(state: IDEPProfilesState = initialState, action: DEPActions): IDEPProfilesState {\n    switch (action.type) {\n        case DEPActionTypes.PROF_INDEX_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case DEPActionTypes.PROF_INDEX_SUCCESS:\n            const payload = action.payload;\n            if (isJSONAPIErrorResponsePayload(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: payload,\n                    loading: false,\n                };\n            } else {\n                return {\n                    ...state,\n                    data: payload.data,\n                    loading: false,\n                };\n            }\n        case DEPActionTypes.PROF_INDEX_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n                loading: false,\n            };\n\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/dep/reducer.ts",
    "content": "import {combineReducers, Reducer} from 'redux';\nimport {account, IDEPAccountState} from \"./account_reducer\";\nimport {accounts, IDEPAccountsState} from \"./accounts_reducer\";\nimport {profiles, IDEPProfilesState} from \"./profiles_reducer\";\nimport {IDEPProfileState, profile} from \"./profile_reducer\";\n\nexport const dep = combineReducers({\n    account,\n    accounts,\n    profile,\n    profiles,\n});\n\nexport interface IDEPState {\n    account?: IDEPAccountState;\n    accounts?: IDEPAccountsState;\n    profiles?: IDEPProfilesState;\n    profile?: IDEPProfileState;\n}\n"
  },
  {
    "path": "ui/src/store/dep/types.ts",
    "content": "\nexport enum DEPAccountOrgVersion {\n    Version2 = \"DEPOrgVersion.v2\",\n}\n\nexport enum DEPAccountOrgType {\n    Education = \"DEPOrgType.Education\",\n}\n\nexport enum SkipSetupSteps {\n    AppleID = \"AppleID\",\n    Biometric = \"Biometric\",\n    Diagnostics = \"Diagnostics\",\n    DisplayTone = \"DisplayTone\",\n    Location = \"Location\",\n    Passcode = \"Passcode\",\n    Payment = \"Payment\",\n    Privacy = \"Privacy\",\n    Restore = \"Restore\",\n    SIMSetup = \"SIMSetup\",\n    Siri = \"Siri\",\n    TOS = \"TOS\",\n    Zoom = \"Zoom\",\n    Android = \"Android\",\n    HomeButtonSensitivity = \"HomeButtonSensitivity\",\n    iMessageAndFaceTime = \"iMessageAndFaceTime\",\n    OnBoarding = \"OnBoarding\",\n    ScreenTime = \"ScreenTime\",\n    SoftwareUpdate = \"SoftwareUpdate\",\n    WatchMigration = \"WatchMigration\",\n    Appearance = \"Appearance\",\n    FileVault = \"FileVault\",\n    iCloudDiagnostics = \"iCloudDiagnostics\",\n    iCloudStorage = \"iCloudStorage\",\n    Registration = \"Registration\",\n    ScreenSaver = \"ScreenSaver\",\n    TapToSetup = \"TapToSetup\",\n    TVHomeScreenSync = \"TVHomeScreenSync\",\n    TVProviderSignIn = \"TVProviderSignIn\",\n    TVRoom = \"TVRoom\",\n}\n\nexport interface DEPAccount {\n    readonly access_token: string;\n    readonly access_token_expiry: string;\n    readonly admin_id: string;\n    readonly consumer_key: string;\n    readonly cursor?: string;\n    readonly facilitator_id: string;\n    readonly fetched_until?: string;\n    readonly more_to_follow: boolean;\n    readonly org_address: string;\n    readonly org_email: string;\n    readonly org_id: string;\n    readonly org_id_hash: string;\n    readonly org_name: string;\n    readonly org_phone: string;\n    readonly org_type: DEPAccountOrgType;\n    readonly org_version: DEPAccountOrgVersion;\n    readonly server_name: string;\n    readonly server_uuid: string;\n    readonly token_updated_at: string;\n    readonly url?: string;\n}\n\nexport interface DEPProfile {\n    readonly id?: string;\n    readonly uuid?: string;\n    dep_account_id?: number;\n\n    profile_name: string;\n    url?: string;\n    allow_pairing: boolean;\n    is_supervised: boolean;\n    is_multi_user: boolean;\n    is_mandatory: boolean;\n    await_device_configured: boolean;\n    is_mdm_removable: boolean;\n    support_phone_number: string;\n    auto_advance_setup: boolean;\n    support_email_address?: string;\n    org_magic?: string;\n    skip_setup_items: SkipSetupSteps[];\n    department?: string;\n\n    // anchor_certs\n    // supervising_host_certs\n}\n"
  },
  {
    "path": "ui/src/store/device/actions.ts",
    "content": "import {Action, Dispatch} from \"redux\";\nimport {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {ThunkAction} from \"redux-thunk\";\nimport {RootState} from \"../../reducers/index\";\nimport {JSON_HEADERS, JSONAPI_HEADERS} from \"../constants\"\nimport {\n    JSONAPIRelationship, JSONAPIRelationships,\n    RSAAChildIndexActionRequest,\n    RSAAIndexActionRequest,\n    RSAAIndexActionResponse, RSAAPatchActionRequest, RSAAReadActionRequest, RSAAReadActionResponse,\n} from \"../json-api\";\nimport {JSONAPIDetailResponse, JSONAPIErrorResponse} from \"../json-api\";\nimport {Tag} from \"../tags/types\";\nimport {Command, Device, DeviceRelationship} from \"./types\";\nimport {\n    encodeJSONAPIChildIndexParameters,\n    encodeJSONAPIIndexParameters,\n    FlaskFilter,\n    FlaskFilters\n} from \"../../flask-rest-jsonapi\";\n\nexport enum DevicesActionTypes {\n    INDEX_REQUEST = \"devices/INDEX_REQUEST\",\n    INDEX_SUCCESS = \"devices/INDEX_SUCCESS\",\n    INDEX_FAILURE = \"devices/INDEX_FAILURE\",\n    READ_REQUEST = \"devices/READ_REQUEST\",\n    READ_SUCCESS = \"devices/READ_SUCCESS\",\n    READ_FAILURE = \"devices/READ_FAILURE\",\n    PATCH_REQUEST = \"devices/PATCH_REQUEST\",\n    PATCH_SUCCESS = \"devices/PATCH_SUCCESS\",\n    PATCH_FAILURE = \"devices/PATCH_FAILURE\",\n    // Relationships\n    COMMANDS_REQUEST = \"devices/COMMANDS_REQUEST\",\n    COMMANDS_SUCCESS = \"devices/COMMANDS_SUCCESS\",\n    COMMANDS_FAILURE = \"devices/COMMANDS_FAILURE\",\n    REL_POST_REQUEST = \"devices/REL_POST_REQUEST\",\n    REL_POST_SUCCESS = \"devices/REL_POST_SUCCESS\",\n    REL_POST_FAILURE = \"devices/REL_POST_FAILURE\",\n    // Interactive methods\n    PUSH_REQUEST = \"devices/PUSH_REQUEST\",\n    PUSH_SUCCESS = \"devices/PUSH_SUCCESS\",\n    PUSH_FAILURE = \"devices/PUSH_FAILURE\",\n    ERASE_REQUEST = \"devices/ERASE_REQUEST\",\n    ERASE_SUCCESS = \"devices/ERASE_SUCCESS\",\n    ERASE_FAILURE = \"devices/ERASE_FAILURE\",\n    LOCK_REQUEST = \"devices/LOCK_REQUEST\",\n    LOCK_SUCCESS = \"devices/LOCK_SUCCESS\",\n    LOCK_FAILURE = \"devices/LOCK_FAILURE\",\n    RESTART_REQUEST = \"devices/RESTART_REQUEST\",\n    RESTART_SUCCESS = \"devices/RESTART_SUCCESS\",\n    RESTART_FAILURE = \"devices/RESTART_FAILURE\",\n    SHUTDOWN_REQUEST = \"devices/SHUTDOWN_REQUEST\",\n    SHUTDOWN_SUCCESS = \"devices/SHUTDOWN_SUCCESS\",\n    SHUTDOWN_FAILURE = \"devices/SHUTDOWN_FAILURE\",\n    CLEARPASSCODE_REQUEST = \"devices/CLEARPASSCODE_REQUEST\",\n    CLEARPASSCODE_SUCCESS = \"devices/CLEARPASSCODE_SUCCESS\",\n    CLEARPASSCODE_FAILURE = \"devices/CLEARPASSCODE_FAILURE\",\n    INVENTORY_REQUEST = \"devices/INVENTORY_REQUEST\",\n    INVENTORY_SUCCESS = \"devices/INVENTORY_SUCCESS\",\n    INVENTORY_FAILURE = \"devices/INVENTORY_FAILURE\",\n\n}\n\nexport type IndexActionRequest = RSAAIndexActionRequest<DevicesActionTypes.INDEX_REQUEST, DevicesActionTypes.INDEX_SUCCESS, DevicesActionTypes.INDEX_FAILURE>;\nexport type IndexActionResponse = RSAAIndexActionResponse<DevicesActionTypes.INDEX_REQUEST, DevicesActionTypes.INDEX_SUCCESS, DevicesActionTypes.INDEX_FAILURE, Device>;\n\nexport const index = encodeJSONAPIIndexParameters((queryParameters: string[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/devices?\" + queryParameters.join(\"&\"),\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                DevicesActionTypes.INDEX_REQUEST,\n                DevicesActionTypes.INDEX_SUCCESS,\n                DevicesActionTypes.INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<\n        DevicesActionTypes.INDEX_REQUEST,\n        DevicesActionTypes.INDEX_SUCCESS,\n        DevicesActionTypes.INDEX_FAILURE>);\n});\n\nexport const fetchDevicesIfRequired = (\n        size: number = 10,\n        pageNumber: number = 1,\n        sort?: string[],\n        filters?: FlaskFilters,\n    ): ThunkAction<void, RootState, void, IndexActionResponse> => (dispatch: Dispatch, getState: () => RootState) => {\n\n    const { auth: { access_token } } = getState();\n\n    // const { devices } = getState();\n    // if (devices.lastReceived) {\n    //     const now = new Date();\n    //     const seconds = 10;\n    //     if ((now.getTime() - devices.lastReceived.getTime()) / 1000 < seconds) {\n    //         console.log(\"cache hit\");\n    //         return;\n    //     }\n    // }\n\n    dispatch(index(size, pageNumber, sort, filters));\n};\n\nexport type ReadActionRequest = RSAAReadActionRequest<\n    DevicesActionTypes.READ_REQUEST, DevicesActionTypes.READ_SUCCESS, DevicesActionTypes.READ_FAILURE>;\nexport type ReadActionResponse = RSAAReadActionResponse<\n    DevicesActionTypes.READ_REQUEST,\n    DevicesActionTypes.READ_SUCCESS,\n    DevicesActionTypes.READ_FAILURE,\n    JSONAPIDetailResponse<Device, undefined>>;\n\nexport const read: ReadActionRequest = (id: string, include?: string[]) => {\n\n    let inclusions = \"\";\n    if (include && include.length) {\n        inclusions = \"include=\" + include.join(\",\")\n    }\n\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${id}?${inclusions}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                DevicesActionTypes.READ_REQUEST,\n                DevicesActionTypes.READ_SUCCESS,\n                DevicesActionTypes.READ_FAILURE,\n            ],\n        },\n    }\n};\n\nexport const READ_CACHE_HIT = \"devices/READ_CACHE_HIT\";\nexport type READ_CACHE_HIT = typeof READ_CACHE_HIT;\n\nexport type CacheFetchActionRequest = (id: string, include?: string[]) => ThunkAction<void, RootState, any, ReadActionResponse>;\n\nexport const fetchDeviceIfRequired = (\n    id: string, include?: string[],\n) => (\n    dispatch: Dispatch,\n    getState: () => RootState,\n) => {\n    const { devices } = getState();\n\n    // if (devices.lastReceived) {\n    //     const now = new Date();\n    //     const seconds = 10;\n    //     if ((now.getTime() - devices.lastReceived.getTime()) / 1000 < seconds) {\n    //         if (devices.byId.hasOwnProperty(id)) {\n    //             dispatch({type: READ_CACHE_HIT, id});\n    //             const payload = {\n    //                 type: READ_SUCCESS,\n    //                 payload: {\n    //                     data: devices.byId[id]\n    //                 }\n    //             };\n    //             dispatch(payload);\n    //             return;\n    //         }\n    //     }\n    // }\n\n    dispatch(read(id, include));\n};\n\nexport type PushActionRequest = (id: string | number) =>\n    RSAAction<DevicesActionTypes.PUSH_REQUEST, DevicesActionTypes.PUSH_SUCCESS, DevicesActionTypes.PUSH_FAILURE>;\n\nexport interface PushActionResponse {\n    type: DevicesActionTypes.PUSH_REQUEST | DevicesActionTypes.PUSH_SUCCESS | DevicesActionTypes.PUSH_FAILURE;\n    payload?: JSONAPIDetailResponse<any, undefined> | JSONAPIErrorResponse;\n}\n\nexport const push: PushActionRequest = (id: string | number) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${id}/push`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                DevicesActionTypes.PUSH_REQUEST,\n                DevicesActionTypes.PUSH_SUCCESS,\n                DevicesActionTypes.PUSH_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type RestartActionRequest = (id: string | number) => RSAAction<\n    DevicesActionTypes.RESTART_REQUEST, DevicesActionTypes.RESTART_SUCCESS, DevicesActionTypes.RESTART_FAILURE>;\n\nexport interface RestartActionResponse {\n    type: DevicesActionTypes.RESTART_REQUEST | DevicesActionTypes.RESTART_SUCCESS | DevicesActionTypes.RESTART_FAILURE;\n    payload?: JSONAPIDetailResponse<any, undefined> | JSONAPIErrorResponse;\n}\n\nexport const restart: RestartActionRequest = (deviceId: string | number) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${deviceId}/restart`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                DevicesActionTypes.RESTART_REQUEST,\n                DevicesActionTypes.RESTART_SUCCESS,\n                DevicesActionTypes.RESTART_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type ShutdownActionRequest = (id: string | number) =>\n    RSAAction<\n        DevicesActionTypes.SHUTDOWN_REQUEST,\n        DevicesActionTypes.SHUTDOWN_SUCCESS,\n        DevicesActionTypes.SHUTDOWN_FAILURE>;\n\nexport interface ShutdownActionResponse {\n    type: DevicesActionTypes.SHUTDOWN_REQUEST |\n          DevicesActionTypes.SHUTDOWN_SUCCESS |\n          DevicesActionTypes.SHUTDOWN_FAILURE;\n    payload?: JSONAPIDetailResponse<any, undefined> | JSONAPIErrorResponse;\n}\n\nexport const shutdown: ShutdownActionRequest = (id: string | number) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${id}/shutdown`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                DevicesActionTypes.SHUTDOWN_REQUEST,\n                DevicesActionTypes.SHUTDOWN_SUCCESS,\n                DevicesActionTypes.SHUTDOWN_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type EraseActionRequest = (id: string | number) =>\n    RSAAction<\n        DevicesActionTypes.ERASE_REQUEST,\n        DevicesActionTypes.ERASE_SUCCESS,\n        DevicesActionTypes.ERASE_FAILURE>;\n\nexport interface EraseActionResponse {\n    type: DevicesActionTypes.ERASE_REQUEST |\n          DevicesActionTypes.ERASE_SUCCESS |\n          DevicesActionTypes.ERASE_FAILURE;\n    payload?: JSONAPIDetailResponse<any, undefined> | JSONAPIErrorResponse;\n}\n\nexport const erase: EraseActionRequest = (id: string | number) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${id}/erase`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                DevicesActionTypes.ERASE_REQUEST,\n                DevicesActionTypes.ERASE_SUCCESS,\n                DevicesActionTypes.ERASE_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type LockActionRequest = (id: string | number, pin?: string, message?: string, phoneNumber?: string) =>\n    RSAAction<\n        DevicesActionTypes.LOCK_REQUEST,\n        DevicesActionTypes.LOCK_SUCCESS,\n        DevicesActionTypes.LOCK_FAILURE>;\n\nexport interface LockActionResponse {\n    type: DevicesActionTypes.LOCK_REQUEST |\n          DevicesActionTypes.LOCK_SUCCESS |\n          DevicesActionTypes.LOCK_FAILURE;\n    payload?: JSONAPIDetailResponse<any, undefined> | JSONAPIErrorResponse;\n}\n\nexport const lock: LockActionRequest = (deviceId: string | number, pin?: string, message?: string, phoneNumber?: string) => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify({\n                message,\n                phoneNumber,\n                pin,\n            }),\n            endpoint: `/api/v1/devices/${deviceId}/lock`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                DevicesActionTypes.LOCK_REQUEST,\n                DevicesActionTypes.LOCK_SUCCESS,\n                DevicesActionTypes.LOCK_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type ClearPasscodeActionRequest = (id: string | number) =>\n    RSAAction<\n        DevicesActionTypes.CLEARPASSCODE_REQUEST,\n        DevicesActionTypes.CLEARPASSCODE_SUCCESS,\n        DevicesActionTypes.CLEARPASSCODE_FAILURE>;\n\nexport interface ClearPasscodeActionResponse {\n    type: DevicesActionTypes.CLEARPASSCODE_REQUEST |\n          DevicesActionTypes.CLEARPASSCODE_SUCCESS |\n          DevicesActionTypes.CLEARPASSCODE_FAILURE;\n    payload?: JSONAPIDetailResponse<any, undefined> | JSONAPIErrorResponse;\n}\n\nexport const clearPasscode: ClearPasscodeActionRequest = (id: string | number) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${id}/clear_passcode`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                DevicesActionTypes.CLEARPASSCODE_REQUEST,\n                DevicesActionTypes.CLEARPASSCODE_SUCCESS,\n                DevicesActionTypes.CLEARPASSCODE_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type InventoryActionRequest = (id: string | number) =>\n    RSAAction<DevicesActionTypes.INVENTORY_REQUEST,\n        DevicesActionTypes.INVENTORY_SUCCESS,\n        DevicesActionTypes.INVENTORY_FAILURE>;\n\nexport interface InventoryActionResponse {\n    type: DevicesActionTypes.INVENTORY_REQUEST |\n          DevicesActionTypes.INVENTORY_SUCCESS |\n          DevicesActionTypes.INVENTORY_FAILURE;\n    payload?: JSONAPIDetailResponse<any, undefined> | JSONAPIErrorResponse;\n}\n\nexport const inventory: InventoryActionRequest = (id: string | number) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/inventory/${id}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                DevicesActionTypes.INVENTORY_REQUEST,\n                DevicesActionTypes.INVENTORY_SUCCESS,\n                DevicesActionTypes.INVENTORY_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type TEST_REQUEST = \"devices/TEST_REQUEST\";\nexport const TEST_REQUEST: TEST_REQUEST = \"devices/TEST_REQUEST\";\nexport type TEST_SUCCESS = \"devices/TEST_SUCCESS\";\nexport const TEST_SUCCESS: TEST_SUCCESS = \"devices/TEST_SUCCESS\";\nexport type TEST_FAILURE = \"devices/TEST_FAILURE\";\nexport const TEST_FAILURE: TEST_FAILURE = \"devices/TEST_FAILURE\";\n\nexport type TestActionRequest = (id: string | number) => RSAAction<TEST_REQUEST, TEST_SUCCESS, TEST_FAILURE>;\nexport interface TestActionResponse {\n    type: TEST_REQUEST | TEST_SUCCESS | TEST_FAILURE;\n    payload?: JSONAPIDetailResponse<any, undefined> | JSONAPIErrorResponse;\n}\n\nexport const test: TestActionRequest = (id: string | number) => {\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/test/${id}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                TEST_REQUEST,\n                TEST_SUCCESS,\n                TEST_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type CommandsActionRequest = RSAAChildIndexActionRequest<\n    DevicesActionTypes.COMMANDS_REQUEST, DevicesActionTypes.COMMANDS_SUCCESS, DevicesActionTypes.COMMANDS_FAILURE>;\nexport type CommandsActionResponse = RSAAIndexActionResponse<\n    DevicesActionTypes.COMMANDS_REQUEST,\n    DevicesActionTypes.COMMANDS_SUCCESS,\n    DevicesActionTypes.COMMANDS_FAILURE,\n    Command>;\n\nexport const commands = encodeJSONAPIChildIndexParameters((deviceId: string, queryParameters: string[])  => {\n    return ({\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${deviceId}/commands?${queryParameters.join(\"&\")}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                DevicesActionTypes.COMMANDS_REQUEST,\n                DevicesActionTypes.COMMANDS_SUCCESS,\n                DevicesActionTypes.COMMANDS_FAILURE,\n            ],\n        },\n    } as RSAAction<\n        DevicesActionTypes.COMMANDS_REQUEST,\n        DevicesActionTypes.COMMANDS_SUCCESS,\n        DevicesActionTypes.COMMANDS_FAILURE>);\n});\n\nexport type PatchActionRequest = RSAAPatchActionRequest<\n    DevicesActionTypes.PATCH_REQUEST, DevicesActionTypes.PATCH_SUCCESS, DevicesActionTypes.PATCH_FAILURE, Device>;\nexport type PatchActionResponse = RSAAReadActionResponse<\n    DevicesActionTypes.PATCH_REQUEST,\n    DevicesActionTypes.PATCH_SUCCESS,\n    DevicesActionTypes.PATCH_FAILURE,\n    JSONAPIDetailResponse<Device, Tag>>;\n\nexport const patch: PatchActionRequest = (deviceId: string, values: Device) => {\n\n    return {\n        [RSAA]: {\n            body: JSON.stringify({\n                data: {\n                    attributes: values,\n                    type: \"devices\",\n                },\n            }),\n            endpoint: `/api/v1/devices/${deviceId}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"PATCH\",\n            types: [\n                DevicesActionTypes.PATCH_REQUEST,\n                DevicesActionTypes.PATCH_SUCCESS,\n                DevicesActionTypes.PATCH_FAILURE,\n            ],\n        },\n    }\n};\n\ntype PostRelationshipActionRequest = (parentId: string, relationship: DeviceRelationship, data: JSONAPIRelationship[]) =>\n    RSAAction<DevicesActionTypes.REL_POST_REQUEST, DevicesActionTypes.REL_POST_SUCCESS, DevicesActionTypes.REL_POST_FAILURE>;\nexport type PostRelationshipActionResponse = RSAAReadActionResponse<\n    DevicesActionTypes.REL_POST_REQUEST,\n    DevicesActionTypes.REL_POST_SUCCESS,\n    DevicesActionTypes.REL_POST_FAILURE,\n    JSONAPIDetailResponse<Device, undefined>>;\n\nexport const postRelationship: PostRelationshipActionRequest = (id: string, relationship: DeviceRelationship, data: JSONAPIRelationship[]) => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify({ data }),\n            endpoint: `/api/v1/devices/${id}/relationships/${relationship}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                DevicesActionTypes.REL_POST_REQUEST,\n                DevicesActionTypes.REL_POST_SUCCESS,\n                DevicesActionTypes.REL_POST_FAILURE,\n            ],\n        },\n    }\n};\n\nexport const RCPOST_REQUEST = \"devices/RCPOST_REQUEST\";\nexport type RCPOST_REQUEST = typeof RCPOST_REQUEST;\nexport const RCPOST_SUCCESS = \"devices/RCPOST_SUCCESS\";\nexport type RCPOST_SUCCESS = typeof RCPOST_SUCCESS;\nexport const RCPOST_FAILURE = \"devices/RCPOST_FAILURE\";\nexport type RCPOST_FAILURE = typeof RCPOST_FAILURE;\n\nexport type PostRelatedActionRequest = <TRelated>(parentId: string, relationship: DeviceRelationship, data: TRelated) => RSAAction<RCPOST_REQUEST, RCPOST_SUCCESS, RCPOST_FAILURE>;\nexport type PostRelatedActionResponse = RSAAReadActionResponse<RCPOST_REQUEST, RCPOST_SUCCESS, RCPOST_FAILURE, JSONAPIDetailResponse<any, undefined>>;\n\nexport const postRelated: PostRelatedActionRequest = <TRelated>(\n    parentId: string,\n    relationship: DeviceRelationship,\n    data: TRelated): RSAAction<RCPOST_REQUEST, RCPOST_SUCCESS, RCPOST_FAILURE> => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify({ data: {\n                    attributes: data,\n                    relationships: {\n                        devices: {\n                            data: [\n                                {\n                                    id: parentId,\n                                    type: \"devices\",\n                                },\n                            ],\n                        },\n                    },\n                    type: relationship,\n                } }),\n            endpoint: `/api/v1/devices/${parentId}/${relationship}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                RCPOST_REQUEST,\n                RCPOST_SUCCESS,\n                RCPOST_FAILURE,\n            ],\n        },\n    }\n};\n\nexport const RPATCH_REQUEST = \"devices/RPATCH_REQUEST\";\nexport type RPATCH_REQUEST = typeof RPATCH_REQUEST;\nexport const RPATCH_SUCCESS = \"devices/RPATCH_SUCCESS\";\nexport type RPATCH_SUCCESS = typeof RPATCH_SUCCESS;\nexport const RPATCH_FAILURE = \"devices/RPATCH_FAILURE\";\nexport type RPATCH_FAILURE = typeof RPATCH_FAILURE;\n\nexport type PatchRelationshipActionRequest = (\n    parentId: string,\n    relationship: DeviceRelationship,\n    data: JSONAPIRelationship[]) => RSAAction<RPATCH_REQUEST, RPATCH_SUCCESS, RPATCH_FAILURE>;\n\nexport type PatchRelationshipActionResponse = RSAAReadActionResponse<\n    RPATCH_REQUEST, RPATCH_SUCCESS, RPATCH_FAILURE, JSONAPIDetailResponse<Device, undefined>>;\n\nexport const patchRelationship: PatchRelationshipActionRequest = (\n    id: string, relationship: DeviceRelationship, data: JSONAPIRelationship[]) => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify({ data }),\n            endpoint: `/api/v1/devices/${id}/relationships/${relationship}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"PATCH\",\n            types: [\n                RPATCH_REQUEST,\n                RPATCH_SUCCESS,\n                RPATCH_FAILURE,\n            ],\n        },\n    }\n};\n"
  },
  {
    "path": "ui/src/store/device/applications.ts",
    "content": "import {JSONAPI_HEADERS} from \"../constants\";\nimport {RSAA, HTTPVerb, RSAAction} from \"redux-api-middleware\";\nimport {\n    RSAAChildIndexActionRequest, RSAAIndexActionResponse\n} from \"../json-api\";\nimport {InstalledApplication} from \"./types\";\nimport {encodeJSONAPIChildIndexParameters} from \"../../flask-rest-jsonapi\";\nimport {RootState} from \"../../reducers\";\n\n\nexport type APPLICATIONS_REQUEST = 'devices/APPLICATIONS_REQUEST';\nexport const APPLICATIONS_REQUEST: APPLICATIONS_REQUEST = 'devices/APPLICATIONS_REQUEST';\nexport type APPLICATIONS_SUCCESS = 'devices/APPLICATIONS_SUCCESS';\nexport const APPLICATIONS_SUCCESS: APPLICATIONS_SUCCESS = 'devices/APPLICATIONS_SUCCESS';\nexport type APPLICATIONS_FAILURE = 'devices/APPLICATIONS_FAILURE';\nexport const APPLICATIONS_FAILURE: APPLICATIONS_FAILURE = 'devices/APPLICATIONS_FAILURE';\n\nexport type InstalledApplicationsActionRequest = RSAAChildIndexActionRequest<APPLICATIONS_REQUEST, APPLICATIONS_SUCCESS, APPLICATIONS_FAILURE>;\nexport type InstalledApplicationsActionResponse = RSAAIndexActionResponse<APPLICATIONS_REQUEST, APPLICATIONS_SUCCESS, APPLICATIONS_FAILURE, InstalledApplication>;\n\n/**\n *\n * @type {(id:number, size?:number, pageNumber?:number, sort?:String[], filters?:FlaskFilters)=>R}\n */\nexport const applications = encodeJSONAPIChildIndexParameters((device_id: string, queryParameters: Array<String>)  => {\n    return (<RSAAction<APPLICATIONS_REQUEST, APPLICATIONS_SUCCESS, APPLICATIONS_FAILURE>>{\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${device_id}/installed_applications?${queryParameters.join('&')}`,\n            method: (<HTTPVerb>'GET'),\n            types: [\n                APPLICATIONS_REQUEST,\n                APPLICATIONS_SUCCESS,\n                APPLICATIONS_FAILURE\n            ],\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n        }\n    });\n});\n"
  },
  {
    "path": "ui/src/store/device/available_os_updates_reducer.ts",
    "content": "import {\n    UPDATES_SUCCESS,\n    AvailableOSUpdatesActionResponse\n} from \"./updates\";\nimport {JSONAPIDataObject, isJSONAPIErrorResponsePayload} from \"../json-api\";\nimport {InstalledProfile} from \"./types\";\nimport {OtherAction} from \"../constants\";\n\n\nexport interface AvailableOSUpdatesState {\n    items?: Array<JSONAPIDataObject<InstalledProfile>>;\n    loading: boolean;\n    pageSize: number;\n    pages: number;\n    recordCount: number;\n}\n\nconst initialState: AvailableOSUpdatesState = {\n    items: [],\n    loading: false,\n    pageSize: 20,\n    pages: 0,\n    recordCount: 0,\n};\n\ntype AvailableOSUpdatesAction = AvailableOSUpdatesActionResponse | OtherAction;\n\nexport function available_os_updates_reducer(\n    state: AvailableOSUpdatesState = initialState,\n    action: AvailableOSUpdatesAction): AvailableOSUpdatesState {\n    switch (action.type) {\n        case UPDATES_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return state;\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    recordCount: action.payload.meta.count,\n                };\n            }\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/device/certificates.ts",
    "content": "import {JSONAPI_HEADERS} from \"../constants\";\nimport {RSAA, HTTPVerb, RSAAction} from \"redux-api-middleware\";\nimport {\n    RSAAChildIndexActionRequest,\n    RSAAIndexActionResponse\n} from \"../json-api\";\nimport {InstalledCertificate} from \"./types\";\nimport {encodeJSONAPIChildIndexParameters} from \"../../flask-rest-jsonapi\";\nimport {RootState} from \"../../reducers\";\n\n\nexport type CERTIFICATES_REQUEST = 'devices/CERTIFICATES_REQUEST';\nexport const CERTIFICATES_REQUEST: CERTIFICATES_REQUEST = 'devices/CERTIFICATES_REQUEST';\nexport type CERTIFICATES_SUCCESS = 'devices/CERTIFICATES_SUCCESS';\nexport const CERTIFICATES_SUCCESS: CERTIFICATES_SUCCESS = 'devices/CERTIFICATES_SUCCESS';\nexport type CERTIFICATES_FAILURE = 'devices/CERTIFICATES_FAILURE';\nexport const CERTIFICATES_FAILURE: CERTIFICATES_FAILURE = 'devices/CERTIFICATES_FAILURE';\n\nexport type CertificatesActionRequest = RSAAChildIndexActionRequest<CERTIFICATES_REQUEST, CERTIFICATES_SUCCESS, CERTIFICATES_FAILURE>;\nexport type CertificatesActionResponse = RSAAIndexActionResponse<CERTIFICATES_REQUEST, CERTIFICATES_SUCCESS, CERTIFICATES_FAILURE, InstalledCertificate>;\n\nexport const certificates = encodeJSONAPIChildIndexParameters((device_id: string, queryParameters: Array<String>)  => {\n    return (<RSAAction<CERTIFICATES_REQUEST, CERTIFICATES_SUCCESS, CERTIFICATES_FAILURE>>{\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${device_id}/installed_certificates?${queryParameters.join('&')}`,\n            method: (<HTTPVerb>'GET'),\n            types: [\n                CERTIFICATES_REQUEST,\n                CERTIFICATES_SUCCESS,\n                CERTIFICATES_FAILURE\n            ],\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n        }\n    });\n});\n"
  },
  {
    "path": "ui/src/store/device/commands_reducer.ts",
    "content": "import {CommandsActionResponse, DevicesActionTypes} from \"./actions\";\nimport {JSONAPIDataObject, isJSONAPIErrorResponsePayload} from \"../json-api\";\nimport {Command} from \"./types\";\nimport {OtherAction} from \"../constants\";\n\nexport interface DeviceCommandsState {\n    items?: Array<JSONAPIDataObject<Command>>;\n    loading: boolean;\n    pageSize: number;\n    pages: number;\n    recordCount: number;\n}\n\nconst initialState: DeviceCommandsState = {\n    items: [],\n    loading: false,\n    pageSize: 20,\n    pages: 0,\n    recordCount: 0,\n};\n\ntype DeviceCommandsAction = CommandsActionResponse | OtherAction;\n\nexport function commands_reducer(state: DeviceCommandsState = initialState, action: DeviceCommandsAction): DeviceCommandsState {\n    switch (action.type) {\n        case DevicesActionTypes.COMMANDS_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return state;\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    pages: Math.floor(action.payload.meta.count / state.pageSize),\n                    recordCount: action.payload.meta.count,\n                };\n            }\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/device/installed_applications_reducer.ts",
    "content": "import {\n    APPLICATIONS_SUCCESS,\n    InstalledApplicationsActionResponse\n} from \"./applications\";\nimport {JSONAPIDataObject, isJSONAPIErrorResponsePayload} from \"../json-api\";\nimport {InstalledApplication} from \"./types\";\nimport {OtherAction} from \"../constants\";\n\nexport interface InstalledApplicationsState {\n    items?: Array<JSONAPIDataObject<InstalledApplication>>;\n    loading: boolean;\n    pageSize: number;\n    pages: number;\n    recordCount: number;\n}\n\nconst initialState: InstalledApplicationsState = {\n    items: [],\n    loading: false,\n    pageSize: 20,\n    pages: 0,\n    recordCount: 0,\n};\n\ntype InstalledCertificatesAction = InstalledApplicationsActionResponse | OtherAction;\n\nexport function installed_applications_reducer(\n    state: InstalledApplicationsState = initialState,\n    action: InstalledCertificatesAction): InstalledApplicationsState {\n    switch (action.type) {\n        case APPLICATIONS_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return state;\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    pages: Math.floor(action.payload.meta.count / state.pageSize),\n                    recordCount: action.payload.meta.count,\n                };\n            }\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/device/installed_certificates_reducer.ts",
    "content": "import {\n    CERTIFICATES_SUCCESS,\n    CertificatesActionResponse\n} from \"./certificates\";\nimport {JSONAPIDataObject, isJSONAPIErrorResponsePayload} from \"../json-api\";\nimport {InstalledCertificate} from \"./types\";\nimport {OtherAction} from \"../constants\";\n\nexport interface InstalledCertificatesState {\n    items?: Array<JSONAPIDataObject<InstalledCertificate>>;\n    loading: boolean;\n    pageSize: number;\n    pages: number;\n    recordCount: number;\n}\n\nconst initialState: InstalledCertificatesState = {\n    items: [],\n    loading: false,\n    pageSize: 20,\n    pages: 0,\n    recordCount: 0,\n};\n\ntype InstalledCertificatesAction = CertificatesActionResponse | OtherAction;\n\nexport function installed_certificates_reducer(\n    state: InstalledCertificatesState = initialState,\n    action: InstalledCertificatesAction,\n): InstalledCertificatesState {\n\n    switch (action.type) {\n        case CERTIFICATES_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return state;\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    pages: Math.floor(action.payload.meta.count / state.pageSize),\n                    recordCount: action.payload.meta.count,\n                };\n            }\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/device/installed_profiles_reducer.ts",
    "content": "import {OtherAction} from \"../constants\";\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\nimport {\n    InstalledProfilesActionResponse,\n    PROFILES_SUCCESS,\n} from \"./profiles\";\nimport {InstalledProfile} from \"./types\";\n\nexport interface InstalledProfilesState {\n    items?: Array<JSONAPIDataObject<InstalledProfile>>;\n    loading: boolean;\n    pageSize: number;\n    pages: number;\n    recordCount: number;\n}\n\nconst initialState: InstalledProfilesState = {\n    items: [],\n    loading: false,\n    pageSize: 20,\n    pages: 0,\n    recordCount: 0,\n};\n\ntype InstalledProfilesAction = InstalledProfilesActionResponse | OtherAction;\n\nexport function installed_profiles_reducer(state: InstalledProfilesState = initialState, action: InstalledProfilesAction): InstalledProfilesState {\n    switch (action.type) {\n        case PROFILES_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return state;\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    recordCount: action.payload.meta.count,\n                };\n            }\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/device/profiles.ts",
    "content": "import {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {encodeJSONAPIChildIndexParameters} from \"../../flask-rest-jsonapi\";\nimport {RootState} from \"../../reducers\";\nimport {JSONAPI_HEADERS} from \"../constants\";\nimport {\n    RSAAChildIndexActionRequest,\n    RSAAIndexActionResponse,\n} from \"../json-api\";\nimport {InstalledApplication} from \"./types\";\n\nexport type PROFILES_REQUEST = \"devices/PROFILES_REQUEST\";\nexport const PROFILES_REQUEST: PROFILES_REQUEST = \"devices/PROFILES_REQUEST\";\nexport type PROFILES_SUCCESS = \"devices/PROFILES_SUCCESS\";\nexport const PROFILES_SUCCESS: PROFILES_SUCCESS = \"devices/PROFILES_SUCCESS\";\nexport type PROFILES_FAILURE = \"devices/PROFILES_FAILURE\";\nexport const PROFILES_FAILURE: PROFILES_FAILURE = \"devices/PROFILES_FAILURE\";\n\nexport type InstalledProfilesActionRequest = RSAAChildIndexActionRequest<PROFILES_REQUEST, PROFILES_SUCCESS, PROFILES_FAILURE>;\nexport type InstalledProfilesActionResponse = RSAAIndexActionResponse<PROFILES_REQUEST, PROFILES_SUCCESS, PROFILES_FAILURE, InstalledApplication>;\n\nexport const profiles = encodeJSONAPIChildIndexParameters((device_id: string, queryParameters: String[])  => {\n    const rsaa: RSAAction<PROFILES_REQUEST, PROFILES_SUCCESS, PROFILES_FAILURE> = {\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${device_id}/installed_profiles?${queryParameters.join(\"&\")}`,\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                PROFILES_REQUEST,\n                PROFILES_SUCCESS,\n                PROFILES_FAILURE,\n            ],\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n        },\n    };\n    return rsaa;\n});\n"
  },
  {
    "path": "ui/src/store/device/reducer.ts",
    "content": "import {InstalledApplicationsActionResponse} from \"./applications\";\nimport {CertificatesActionResponse} from \"./certificates\";\nimport * as actions from \"./actions\";\nimport {\n    CommandsActionResponse, PatchRelationshipActionResponse, PostRelatedActionResponse,\n    ReadActionResponse,\n} from \"./actions\";\nimport {DevicesActionTypes} from \"./actions\";\nimport {isArray} from \"../../guards\";\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\nimport {Tag} from \"../tags/types\";\nimport {available_os_updates_reducer, AvailableOSUpdatesState} from \"./available_os_updates_reducer\";\nimport {commands_reducer, DeviceCommandsState} from \"./commands_reducer\";\nimport {installed_applications_reducer, InstalledApplicationsState} from \"./installed_applications_reducer\";\nimport {installed_certificates_reducer, InstalledCertificatesState} from \"./installed_certificates_reducer\";\nimport {installed_profiles_reducer, InstalledProfilesState} from \"./installed_profiles_reducer\";\nimport {Device} from \"./types\";\n\nexport interface DeviceState {\n    device?: JSONAPIDataObject<Device>;\n    loading: boolean;\n    error: boolean;\n    errorDetail?: any\n    lastReceived?: Date;\n    currentPage: number;\n    pageSize: number;\n    recordCount?: number;\n    commands?: DeviceCommandsState;\n    installed_certificates?: InstalledCertificatesState;\n    installed_applications?: InstalledApplicationsState;\n    installed_profiles?: InstalledProfilesState;\n    available_os_updates?: AvailableOSUpdatesState;\n    tags?: Array<JSONAPIDataObject<Tag>>;\n    tagsLoading: boolean;\n}\n\nconst initialState: DeviceState = {\n    currentPage: 1,\n    device: null,\n    error: false,\n    errorDetail: null,\n    lastReceived: null,\n    loading: false,\n    pageSize: 50,\n    tagsLoading: false,\n};\n\ntype DevicesAction = ReadActionResponse | InstalledApplicationsActionResponse | CommandsActionResponse |\n    CertificatesActionResponse | PatchRelationshipActionResponse | PostRelatedActionResponse;\n\nexport function device(state: DeviceState = initialState, action: DevicesAction): DeviceState {\n    switch (action.type) {\n        case DevicesActionTypes.READ_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n\n        case DevicesActionTypes.READ_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n            };\n\n        case DevicesActionTypes.READ_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                }\n            } else {\n                let tags: Array<JSONAPIDataObject<Tag>> = [];\n\n                if (action.payload.included) {\n                    tags = action.payload.included.filter((included: JSONAPIDataObject<any>) => (included.type === \"tags\"));\n                }\n\n                return {\n                    ...state,\n                    device: action.payload.data,\n                    lastReceived: new Date,\n                    loading: false,\n                    tags,\n                };\n            }\n        case actions.RPATCH_REQUEST:\n            return {\n                ...state,\n                tagsLoading: true,\n            };\n        case actions.RPATCH_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                }\n            } else {\n                const device: JSONAPIDataObject<Device> = {\n                    ...state.device,\n                    relationships: {\n                        ...state.device.relationships,\n                        // tags: action.payload.data.relationships.tags,\n                    },\n                };\n\n                return {\n                    ...state,\n                    device,\n                    tagsLoading: false,\n                };\n            }\n\n        case actions.RPATCH_FAILURE:\n            return {\n                ...state,\n                tagsLoading: false,\n            };\n\n        case actions.RCPOST_REQUEST:\n            return {\n                ...state,\n                tagsLoading: true,\n            };\n\n        case actions.RCPOST_SUCCESS:\n            const payload = action.payload;\n\n            if (isJSONAPIErrorResponsePayload(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                }\n\n            } else {\n                const data: any[] = isArray(payload.data) ? payload.data : [payload.data];\n                const mergedTags = data.concat(state.device.relationships.tags.data);\n\n                return {\n                    ...state,\n                    device: {\n                        ...state.device,\n                        relationships: {\n                            ...state.device.relationships,\n                            tags: {\n                                data: mergedTags,\n                            },\n                        },\n                    },\n                };\n            }\n        case actions.RCPOST_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n                tagsLoading: false,\n            };\n\n        default:\n            return {\n                ...state,\n                available_os_updates: available_os_updates_reducer(state.available_os_updates, action),\n                commands: commands_reducer(state.commands, action),\n                installed_applications: installed_applications_reducer(state.installed_applications, action),\n                installed_certificates: installed_certificates_reducer(state.installed_certificates, action),\n                installed_profiles: installed_profiles_reducer(state.installed_profiles, action),\n            };\n    }\n}\n"
  },
  {
    "path": "ui/src/store/device/types.ts",
    "content": "// Valid JSON-API relationships\nexport type DeviceRelationship = \"commands\" | \"tags\" | \"groups\" | \"profiles\";\n\nexport interface Device {\n    udid: string;\n    // topic\n    last_seen: string;\n    is_enrolled: boolean;\n    build_version: string;\n    device_name: string;\n    model: string;\n    model_name: string;\n    os_version: string;\n    product_name: string;\n    serial_number: string;\n    hostname: string;\n    local_hostname: string;\n    available_device_capacity: number;\n    device_capacity: number;\n    wifi_mac: string;\n    bluetooth_mac: string;\n    awaiting_configuration: boolean;\n    // push_magic\n    // tokenupdate_at\n    // last_apns_id\n\n    // last_push_at\n    passcode_present: boolean;\n    passcode_compliant: boolean;\n    passcode_compliant_with_profiles: boolean;\n    fde_enabled: boolean;\n    fde_has_prk: boolean;\n    fde_has_irk: boolean;\n    firewall_enabled: boolean;\n    block_all_incoming: boolean;\n    stealth_mode_enabled: boolean;\n    sip_enabled: boolean;\n    battery_level: number;\n    carrier_settings_version: string;\n    cellular_technology: string;\n    current_carrier_network: string;\n    current_mcc: string;\n    current_mnc: string;\n    data_roaming_enabled: boolean;\n    eas_device_identifier: string;\n    iccid: string;\n    imei: string;\n    is_activation_lock_enabled: boolean;\n    is_cloud_backup_enabled: boolean;\n    is_device_locator_service_enabled: boolean;\n    is_do_not_disturb_in_effect: boolean;\n    is_mdm_lost_mode_enabled: boolean;\n    is_roaming: boolean;\n    is_supervised: boolean;\n    itunes_store_account_hash: string;\n    itunes_store_account_is_active: boolean;\n    last_cloud_backup_date: string;\n    maximum_resident_users: number;\n    meid: string;\n    modem_firmware_version: string;\n    passcode_lock_grace_period_enforced: number;\n    personal_hotspot_enabled: boolean;\n    phone_number: string;\n    sim_carrier_network: string;\n    subscriber_carrier_network: string;\n    subscriber_mcc: string;\n    subscriber_mnc: string;\n    voice_roaming_enabled: boolean;\n\n    // DEP\n    dep_profile_id?: number;\n    description?: string;\n    asset_tag?: string;\n    color?: string;\n    device_assigned_by?: string;\n    device_assigned_date?: string;\n    device_family?: string;\n    is_dep: boolean;\n    os?: string;\n    profile_assign_time?: string;\n    profile_push_time?: string;\n    profile_status?: string;\n    profile_uuid?: string;\n}\n\nexport interface InstalledCertificate {\n    x509_cn: string;\n    is_identity: boolean;\n    fingerprint_sha256: string;\n}\n\nexport interface InstalledApplication {\n    id?: number;\n    device_udid: string;\n    device_id: number;\n    bundle_identifier: string;\n    version: string;\n    short_version: string;\n    name: string;\n    bundle_size: number;\n    dynamic_size: number;\n    is_validated: boolean;\n}\n\nexport interface InstalledProfile {\n    id?: number;\n    device_udid: string;\n    device_id: number;\n    has_removal_password: boolean;\n    is_encrypted: boolean;\n    payload_description: string;\n    payload_display_name: string;\n    payload_identifier: string;\n    payload_organization: string;\n    payload_removal_disallowed: boolean;\n    payload_uuid: string;\n}\n\nexport interface AvailableOSUpdate {\n    id?: string;\n    allows_install_later: boolean;\n    app_identifiers_to_close: string[];\n    human_readable_name: string;\n    human_readable_name_locale: string;\n    is_config_data_update: boolean;\n    is_critical: boolean;\n    is_firmware_update: boolean;\n    metadata_url: string;\n    product_key: string;\n    restart_required: boolean;\n    version: string;\n}\n\nexport enum MDMCommandType {\n    CertificateList = \"CertificateList\",\n    ClearPasscode = \"ClearPasscode\",\n    DeviceInformation = \"DeviceInformation\",\n    DeviceLock = \"DeviceLock\",\n    InstalledApplicationList = \"InstalledApplicationList\",\n    InstallProfile = \"InstallProfile\",\n    InstallProvisioningProfile = \"InstallProvisioningProfile\",\n    ProfileList = \"ProfileList\",\n    ProvisioningProfileList = \"ProvisioningProfileList\",\n    RemoveProfile = \"RemoveProfile\",\n    RemoveProvisioningProfile = \"RemoveProvisioningProfile\",\n    RestartDevice = \"RestartDevice\",\n    SecurityInfo = \"SecurityInfo\",\n    ShutDownDevice = \"ShutDownDevice\",\n}\n\nexport interface Command {\n    id?: number;\n    command_class?: MDMCommandType;\n    uuid?: string;\n    input_data?: string;\n    queued_status: string;\n    queued_at?: Date;\n    sent_at?: Date;\n    acknowledged_at?: Date;\n    after?: Date;\n    ttl: number;\n}\n\nexport enum DeviceModelName {\n    iPad = \"iPad\",\n    iPhone = \"iPhone\",\n    MacMini = \"Mac Mini\",\n    MacPro = \"Mac Pro\",\n}\n\nexport enum DeviceOperatingSystem {\n    iOS = \"iOS\",\n    macOS = \"macOS\",\n    tvOS = \"tvOS\",\n    watchOS = \"watchOS\",\n    Unknown = \"Unknown\",\n}\n\nexport function operatingSystem(model: DeviceModelName): DeviceOperatingSystem {\n    switch (model) {\n        case DeviceModelName.iPad:\n        case DeviceModelName.iPhone:\n            return DeviceOperatingSystem.iOS;\n        case DeviceModelName.MacMini:\n        case DeviceModelName.MacPro:\n            return DeviceOperatingSystem.macOS;\n        default:\n            return DeviceOperatingSystem.Unknown;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/device/updates.ts",
    "content": "import {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {\n    RSAAChildIndexActionRequest,\n    RSAAIndexActionResponse,\n} from \"../json-api\";\nimport {AvailableOSUpdate} from \"./types\";\nimport {JSONAPI_HEADERS} from \"../constants\";\nimport {encodeJSONAPIChildIndexParameters} from \"../../flask-rest-jsonapi\";\nimport {RootState} from \"../../reducers\";\n\nexport const UPDATES_REQUEST = \"devices/UPDATES_REQUEST\";\nexport type UPDATES_REQUEST = typeof UPDATES_REQUEST;\nexport const UPDATES_SUCCESS = \"devices/UPDATES_SUCCESS\";\nexport type UPDATES_SUCCESS = typeof UPDATES_SUCCESS;\nexport const UPDATES_FAILURE = \"devices/UPDATES_FAILURE\";\nexport type UPDATES_FAILURE = typeof UPDATES_FAILURE;\n\nexport type AvailableOSUpdatesActionRequest = RSAAChildIndexActionRequest<UPDATES_REQUEST, UPDATES_SUCCESS, UPDATES_FAILURE>;\nexport type AvailableOSUpdatesActionResponse = RSAAIndexActionResponse<UPDATES_REQUEST, UPDATES_SUCCESS, UPDATES_FAILURE, AvailableOSUpdate>;\n\nexport const updates = encodeJSONAPIChildIndexParameters((deviceId: string, queryParameters: string[])  => {\n    return ({\n        [RSAA]: {\n            endpoint: `/api/v1/devices/${deviceId}/available_os_updates?${queryParameters.join(\"&\")}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                UPDATES_REQUEST, UPDATES_SUCCESS, UPDATES_FAILURE,\n            ],\n        },\n    } as RSAAction<UPDATES_REQUEST, UPDATES_SUCCESS, UPDATES_FAILURE>);\n});\n"
  },
  {
    "path": "ui/src/store/device_groups/actions.ts",
    "content": "import {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {\n    JSONAPIDataObject, JSONAPIDetailResponse,\n    JSONAPIListResponse,\n    RSAAIndexActionRequest,\n    RSAAIndexActionResponse, RSAAReadActionRequest, RSAAReadActionResponse,\n} from \"../json-api\";\nimport {DeviceGroup} from \"./types\";\nimport {JSON_HEADERS, JSONAPI_HEADERS} from \"../constants\"\nimport {Device} from \"../device/types\";\nimport {\n    encodeJSONAPIChildIndexParameters,\n    encodeJSONAPIIndexParameters,\n    FlaskFilter,\n    FlaskFilters\n} from \"../../flask-rest-jsonapi\";\nimport {RootState} from \"../../reducers\";\n\nexport type INDEX_REQUEST = \"device_groups/INDEX_REQUEST\";\nexport const INDEX_REQUEST: INDEX_REQUEST = \"device_groups/INDEX_REQUEST\";\nexport type INDEX_SUCCESS = \"device_groups/INDEX_SUCCESS\";\nexport const INDEX_SUCCESS: INDEX_SUCCESS = \"device_groups/INDEX_SUCCESS\";\nexport type INDEX_FAILURE = \"device_groups/INDEX_FAILURE\";\nexport const INDEX_FAILURE: INDEX_FAILURE = \"device_groups/INDEX_FAILURE\";\n\nexport type IndexActionRequest = RSAAIndexActionRequest<INDEX_REQUEST, INDEX_SUCCESS, INDEX_FAILURE>;\nexport type IndexActionResponse = RSAAIndexActionResponse<INDEX_REQUEST, INDEX_SUCCESS, INDEX_FAILURE, DeviceGroup>;\n\nexport const index = encodeJSONAPIIndexParameters((queryParameters: String[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/device_groups?\" + queryParameters.join(\"&\"),\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                INDEX_REQUEST,\n                INDEX_SUCCESS,\n                INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<INDEX_REQUEST, INDEX_SUCCESS, INDEX_FAILURE>);\n});\n\nexport type READ_REQUEST = \"device_groups/READ_REQUEST\";\nexport const READ_REQUEST: READ_REQUEST = \"device_groups/READ_REQUEST\";\nexport type READ_SUCCESS = \"device_groups/READ_SUCCESS\";\nexport const READ_SUCCESS: READ_SUCCESS = \"device_groups/READ_SUCCESS\";\nexport type READ_FAILURE = \"device_groups/READ_FAILURE\";\nexport const READ_FAILURE: READ_FAILURE = \"device_groups/READ_FAILURE\";\n\nexport type ReadActionRequest = RSAAReadActionRequest<READ_REQUEST, READ_SUCCESS, READ_FAILURE>;\nexport type ReadActionResponse = RSAAReadActionResponse<READ_REQUEST, READ_SUCCESS, READ_FAILURE, JSONAPIDetailResponse<DeviceGroup, undefined>>;\n\nexport const read: ReadActionRequest = (id: string, include?: string[]) => {\n\n    let inclusions = \"\";\n    if (include && include.length) {\n        inclusions = \"include=\" + include.join(\",\")\n    }\n\n    return ({\n        [RSAA]: {\n            endpoint: `/api/v1/device_groups/${id}?${inclusions}`,\n            method: \"GET\",\n            types: [\n                READ_REQUEST,\n                READ_SUCCESS,\n                READ_FAILURE,\n            ],\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n        },\n    } as RSAAction<READ_REQUEST, READ_SUCCESS, READ_FAILURE>);\n};\n\nexport type POST_REQUEST = \"device_groups/POST_REQUEST\";\nexport const POST_REQUEST: POST_REQUEST = \"device_groups/POST_REQUEST\";\nexport type POST_SUCCESS = \"device_groups/POST_SUCCESS\";\nexport const POST_SUCCESS: POST_SUCCESS = \"device_groups/POST_SUCCESS\";\nexport type POST_FAILURE = \"device_groups/POST_FAILURE\";\nexport const POST_FAILURE: POST_FAILURE = \"device_groups/POST_FAILURE\";\n\ntype PostActionRequest = (values: DeviceGroup) => RSAAction<POST_REQUEST, POST_SUCCESS, POST_FAILURE>;\n\nexport interface PostActionResponse {\n    type: POST_REQUEST | POST_FAILURE | POST_SUCCESS;\n    payload?: JSONAPIListResponse<JSONAPIDataObject<DeviceGroup>>;\n}\n\nexport const post: PostActionRequest = (values: DeviceGroup) => {\n\n    return {\n        [RSAA]: {\n            body: JSON.stringify({\n                data: {\n                    attributes: values,\n                    type: \"reducer\",\n                },\n            }),\n            endpoint: `/api/v1/device_groups`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                POST_REQUEST,\n                POST_SUCCESS,\n                POST_FAILURE,\n            ],\n        },\n    }\n};\n"
  },
  {
    "path": "ui/src/store/device_groups/reducer.ts",
    "content": "import {\n    INDEX_SUCCESS, IndexActionResponse,\n    READ_SUCCESS, ReadActionResponse\n} from \"./actions\";\nimport {JSONAPIDataObject, isJSONAPIErrorResponsePayload, JSONAPIDetailResponse} from \"../json-api\";\nimport {DeviceGroup} from \"./types\";\nimport {Device} from \"../device/types\";\n\nexport interface DeviceGroupsState {\n    items?: Array<JSONAPIDataObject<DeviceGroup>>;\n    editing?: JSONAPIDetailResponse<DeviceGroup, Device>;\n    recordCount: number;\n}\n\nconst initialState: DeviceGroupsState = {\n    items: [],\n    recordCount: 0\n};\n\ntype DeviceGroupsAction = IndexActionResponse | ReadActionResponse;\n\nexport function device_groups(state: DeviceGroupsState = initialState, action: DeviceGroupsAction): DeviceGroupsState {\n    switch (action.type) {\n        case INDEX_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return state;\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    recordCount: action.payload.meta.count\n                };\n            }\n        case READ_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return state;\n            } else {\n                return {\n                    ...state,\n                    editing: action.payload\n                };\n            }\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/device_groups/types.ts",
    "content": "export interface DeviceGroup {\n    id?: string;\n    name: string;\n}\n"
  },
  {
    "path": "ui/src/store/devices/actions.ts",
    "content": ""
  },
  {
    "path": "ui/src/store/devices/devices.ts",
    "content": "import {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\nimport {\n    DevicesActionTypes,\n    IndexActionResponse,\n} from \"../device/actions\";\nimport {Device} from \"../device/types\";\n\nexport interface IDeviceIdMap {\n    [deviceId: string]: JSONAPIDataObject<Device>;\n}\n\nexport interface IDevicesState {\n    items: Array<JSONAPIDataObject<Device>>;\n    byId: IDeviceIdMap;\n    allIds: string[];\n    loading: boolean;\n    error: boolean;\n    errorDetail?: any\n    lastReceived?: Date;\n    currentPage: number;\n    pageSize: number;\n    recordCount?: number;\n}\n\nconst initialState: IDevicesState = {\n    allIds: [],\n    byId: {},\n    currentPage: 1,\n    error: false,\n    errorDetail: null,\n    items: [],\n    lastReceived: null,\n    loading: false,\n    pageSize: 50,\n};\n\ntype DevicesAction = IndexActionResponse;\n\nexport function devices(state: IDevicesState = initialState, action: DevicesAction): IDevicesState {\n    switch (action.type) {\n        case DevicesActionTypes.INDEX_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n\n        case DevicesActionTypes.INDEX_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n            };\n\n        case DevicesActionTypes.INDEX_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                    loading: false,\n                }\n            } else {\n                const allIds: string[] = [];\n                const byId: IDeviceIdMap = action.payload.data.reduce((memo: IDeviceIdMap, device: JSONAPIDataObject<Device>) => {\n                    memo[device.id] = device;\n                    allIds.push(\"\" + device.id);\n                    return memo;\n                }, {});\n\n                return {\n                    ...state,\n                    allIds,\n                    byId,\n                    items: action.payload.data,\n                    lastReceived: new Date,\n                    loading: false,\n                    recordCount: action.payload.meta.count,\n                };\n            }\n\n        // case actions.DELETE_REQUEST:\n        //     return {\n        //         ...state,\n        //         loading: true\n        //     };\n        //\n        // case actions.DELETE_FAILURE:\n        //     return {\n        //         ...state,\n        //         loading: false,\n        //         error: true,\n        //         errorDetail: action.payload\n        //     };\n        //\n        // case actions.DELETE_SUCCESS:\n        //     return state;\n\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/json-api.ts",
    "content": "// Redux API Middleware Type Guards\nimport {ApiError, InvalidRSAA, RequestError, RSAAction} from \"redux-api-middleware\";\nimport {FlaskFilters} from \"../flask-rest-jsonapi\";\nimport {Action} from \"redux\";\nimport {Relationships} from \"../json-api-v1\";\n\n\nexport const JSONAPI_HEADERS = {\n    \"Accept\": \"application/vnd.api+json\",\n    \"Content-Type\": \"application/vnd.api+json\",\n};\n\n// http://jsonapi.org/format/#errors\nexport interface JSONAPIErrorObject {\n    id?: any;\n    links?: {\n        about?: string;\n    };\n    status?: string;\n    code?: string;\n    title?: string;\n    detail?: string;\n    source?: {\n        pointer?: string;\n        parameter?: string;\n    };\n    meta?: any;\n}\n\nexport interface JSONAPIErrorResponse {\n    errors: JSONAPIErrorObject[];\n    jsonapi: {\n        version: string;\n    };\n}\n\nexport interface JSONAPIResourceIdentifier {\n    type: string;\n    id: string;\n    meta?: {\n        [index: string]: any;\n    };\n}\n\nexport type JSONAPILink = string | { href?: string, meta?: { [index: string]: any }};\n\nexport interface JSONAPILinks {\n    self?: JSONAPILink;\n    related?: JSONAPILink;\n\n    // Pagination\n    first?: JSONAPILink;\n    last?: JSONAPILink;\n    prev?: JSONAPILink;\n    next?: JSONAPILink;\n}\n\nexport interface JSONAPIRelationship {\n    data: JSONAPIResourceIdentifier[] | JSONAPIResourceIdentifier | null,\n    links?: JSONAPILinks;\n}\n\nexport interface JSONAPIRelationships {\n    [relationshipName: string]: JSONAPIRelationship;\n}\n\nexport interface JSONAPIDataObject<TObject> {\n    id: string|number;\n    type: string;\n    attributes: TObject;\n    relationships?: JSONAPIRelationships;\n    links?: JSONAPILinks;\n}\n\nexport interface JSONAPIDataResponse<TData, TIncluded> {\n    data?: TData;\n    links?: JSONAPILinks;\n    meta?: {\n        count?: number;\n    };\n    jsonapi?: {\n        version: string;\n    };\n}\n\nexport interface JSONAPIDetailResponse<TObject, TIncluded> {\n    data?: JSONAPIDataObject<TObject>;\n    included?: Array<JSONAPIDataObject<TIncluded>>;\n    links?: JSONAPILinks;\n    meta?: {\n        count?: number;\n    };\n    jsonapi?: {\n        version: string;\n    };\n}\n\nexport interface JSONAPIListResponse<TObject> {\n    data?: TObject[];\n    links?: JSONAPILinks;\n    meta?: {\n        count?: number;\n    };\n    jsonapi?: {\n        version: string;\n    };\n}\n\nexport function isJSONAPIErrorResponsePayload(\n    payload: JSONAPIListResponse<any> |\n        JSONAPIDetailResponse<any, any> |\n        JSONAPIErrorResponse): payload is JSONAPIErrorResponse {\n\n    return (payload as JSONAPIErrorResponse).errors !== undefined;\n}\n\n\n\n// Standardised JSON-API Index ActionCreator\nexport type RSAAIndexActionRequest<TRequest, TSuccess, TFailure> =\n    (size?: number, pageNumber?: number, sort?: string[], filters?: FlaskFilters) => RSAAction<TRequest, TSuccess, TFailure>;\n\n// Standardised JSON-API Index ActionCreator Response (passed to reducer)\n\nexport interface RSAAResponseRequest<TRequest> extends Action<TRequest> {\n    error?: boolean;\n    payload?: InvalidRSAA | RequestError;\n    type: TRequest;\n}\n\nexport interface RSAAResponseFailure<TFailure> extends Action<TFailure> {\n    payload: ApiError;\n    type: TFailure;\n}\n\n// Success can still contain API errors that are 2xx responses\nexport interface RSAAResponseSuccess<TSuccess, TResponse> extends Action<TSuccess> {\n    payload: TResponse;\n    type: TSuccess;\n}\n\nexport type RSAAIndexActionResponse<TRequest, TSuccess, TFailure, TObject> =\n    RSAAResponseRequest<TRequest> |\n    RSAAResponseFailure<TFailure> |\n    RSAAResponseSuccess<TSuccess, JSONAPIListResponse<JSONAPIDataObject<TObject>>> |\n    RSAAResponseSuccess<TSuccess, JSONAPIErrorResponse>;\n\n// Standardised JSON-API Index ActionCreator that fetches a resource child of some object / by relationship\nexport type RSAAChildIndexActionRequest<TRequest, TSuccess, TFailure> =\n    (parent_id: string, size?: number, pageNumber?: number, sort?: string[], filters?: FlaskFilters)\n        => RSAAction<TRequest, TSuccess, TFailure>;\n\nexport type RSAAReadActionRequest<TRequest, TSuccess, TFailure> = (id: string, include?: string[])\n    => RSAAction<TRequest, TSuccess, TFailure>;\n\nexport interface RSAAReadActionResponseSuccess<TSuccess, TResponse> {\n    type: TSuccess;\n    payload: TResponse;\n}\n\nexport type RSAAReadActionResponse<TRequest, TSuccess, TFailure, TResponse> = RSAAResponseRequest<TRequest> |\n    RSAAResponseFailure<TFailure> |\n    RSAAReadActionResponseSuccess<TSuccess, TResponse> |\n    RSAAReadActionResponseSuccess<TSuccess, JSONAPIErrorResponse>;\n\nexport type RSAAPostActionRequest<TRequest, TSuccess, TFailure, TValues> = (\n    values: TValues, relationships?: Relationships)\n    => RSAAction<TRequest, TSuccess, TFailure>;\n\nexport type RSAAPostActionResponse<TRequest, TSuccess, TFailure, TResponse> = RSAAResponseRequest<TRequest> |\n    RSAAResponseFailure<TFailure> | RSAAResponseSuccess<TSuccess, TResponse | JSONAPIErrorResponse>;\n\nexport type RSAAPatchActionRequest<TRequest, TSuccess, TFailure, TValues> = (id: string, values: TValues)\n    => RSAAction<TRequest, TSuccess, TFailure>;\n\nexport type RSAADeleteActionRequest<TRequest, TSuccess, TFailure> = (id: string) => RSAAction<TRequest, TSuccess, TFailure>;\n\nexport interface RSAADeleteActionResponseSuccess<TSuccess> {\n    type: TSuccess;\n}\n\nexport type RSAADeleteActionResponse<TRequest, TSuccess, TFailure, TResponse> = RSAAResponseRequest<TRequest> |\n    RSAAResponseFailure<TFailure> | RSAADeleteActionResponseSuccess<TSuccess>;\n\n"
  },
  {
    "path": "ui/src/store/mdm.ts",
    "content": "namespace MDM {\n    export interface Command {\n        RequestType: string;\n    }\n\n    export interface InstallProfile extends Command {\n        Payload: any;\n    }\n\n    export interface RemoveProfile extends Command {\n        Identifier: string;\n    }\n\n    export interface InstallProvisioningProfile extends Command {\n        ProvisioningProfile: any;\n    }\n\n    export interface RemoveProvisioningProfile extends Command {\n        Identifier: string;\n    }\n\n    export interface InstalledApplicationList extends Command {\n        Identifiers?: Array<string>;\n        ManagedAppsOnly?: boolean;\n    }\n\n    export interface DeviceInformation extends Command {\n        Queries: Array<string>;\n    }\n\n    export interface DeviceLock extends Command {\n        PIN: string;\n        Message?: string;\n        PhoneNumber?: string;\n    }\n\n    export interface ClearPasscode extends Command {\n        UnlockToken: any;\n    }\n\n    export interface EraseDevice extends Command {\n        PIN: string;\n    }\n\n    export interface RequestMirroring extends Command {\n        DestinationName?: string;\n        DestinationDeviceID?: string;\n        ScanTime?: number;\n        Password?: string;\n    }\n\n    export interface Restrictions extends Command {\n        ProfileRestrictions?: boolean;\n    }\n\n    export interface DeleteUser extends Command {\n        UserName: string;\n        ForceDeletion?: boolean;\n    }\n\n    export interface InstallApplicationOptions {\n        NotManaged: boolean;\n        PurchaseMethod: number;\n    }\n\n    export interface InstallApplication extends Command {\n        iTunesStoreID: number;\n        Identifier?: string;\n        Options?: InstallApplicationOptions;\n        ManifestURL: string;\n        ManagementFlags: number;\n        Configuration?: any;\n        Attributes?: any;\n        ChangeManagementState?: 'Managed';\n    }\n\n    export interface ApplyRedemptionCode extends Command {\n        Identifier: string;\n        RedemptionCode: string;\n    }\n\n    export interface ManagedApplicationList extends Command {\n        Identifiers?: Array<string>;\n    }\n\n    export interface RemoveApplication extends Command {\n        Identifier: string;\n    }\n\n    export interface InviteToProgram extends Command {\n        ProgramID: 'com.apple.cloudvpp';\n        InvitationURL: string;\n    }\n\n    export interface ValidateApplications extends Command {\n        Identifiers?: Array<string>;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/organization/actions.ts",
    "content": "import { RSAA, RSAAction } from \"redux-api-middleware\";\nimport {RSAAReadActionRequest, RSAAReadActionResponse} from \"../json-api\";\nimport {JSON_HEADERS, JSONAPI_HEADERS} from \"../constants\"\nimport {Organization} from \"./types\";\n\nexport type READ_REQUEST = \"organization/READ_REQUEST\";\nexport const READ_REQUEST: READ_REQUEST = \"organization/READ_REQUEST\";\nexport type READ_SUCCESS = \"organization/READ_SUCCESS\";\nexport const READ_SUCCESS: READ_SUCCESS = \"organization/READ_SUCCESS\";\nexport type READ_FAILURE = \"organization/READ_FAILURE\";\nexport const READ_FAILURE: READ_FAILURE = \"organization/READ_FAILURE\";\n\nexport type ReadActionRequest = () => RSAAction<READ_REQUEST, READ_SUCCESS, READ_FAILURE>;\n\nexport type ReadActionResponse = RSAAReadActionResponse<READ_REQUEST, READ_SUCCESS, READ_FAILURE, Organization>;\n\nexport const read: ReadActionRequest = () => {\n    return {\n        [RSAA]: {\n            endpoint: \"/api/v1/configuration/organization\",\n            headers: JSON_HEADERS,\n            method: \"GET\",\n            types: [\n                READ_REQUEST,\n                READ_SUCCESS,\n                READ_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type POST_REQUEST = \"organization/POST_REQUEST\";\nexport const POST_REQUEST: POST_REQUEST = \"organization/POST_REQUEST\";\nexport type POST_SUCCESS = \"organization/POST_SUCCESS\";\nexport const POST_SUCCESS: POST_SUCCESS = \"organization/POST_SUCCESS\";\nexport type POST_FAILURE = \"organization/POST_FAILURE\";\nexport const POST_FAILURE: POST_FAILURE = \"organization/POST_FAILURE\";\n\nexport type PostActionRequest = (values: Organization) => RSAAction<POST_REQUEST, POST_SUCCESS, POST_FAILURE>;\n\nexport interface PostActionResponse {\n    type: POST_REQUEST | POST_FAILURE | POST_SUCCESS;\n    payload?: Organization;\n}\n\nexport const post: PostActionRequest = (values: Organization) => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify(values),\n            endpoint: \"/api/v1/configuration/organization\",\n            headers: JSON_HEADERS,\n            method: \"POST\",\n            types: [\n                POST_REQUEST,\n                POST_SUCCESS,\n                POST_FAILURE,\n            ],\n        },\n    }\n};\n"
  },
  {
    "path": "ui/src/store/organization/reducer.ts",
    "content": "import * as actions from \"./actions\";\nimport {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\nimport {Organization} from \"./types\";\nimport {isApiError} from \"../../guards\";\n\nexport interface OrganizationState {\n    organization?: Organization;\n    loading: boolean;\n    error: boolean;\n    errorDetail?: any\n    lastReceived?: Date;\n    submitted: boolean;\n}\n\nconst initialState: OrganizationState = {\n    error: false,\n    loading: false,\n    submitted: false,\n};\n\nexport type OrganizationAction = actions.ReadActionResponse | actions.PostActionResponse;\n\nexport function organization(state: OrganizationState = initialState, action: OrganizationAction): OrganizationState {\n    switch (action.type) {\n        case actions.POST_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case actions.POST_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n                loading: false,\n            };\n        case actions.POST_SUCCESS:\n            return {\n                ...state,\n                loading: false,\n                organization: action.payload,\n                submitted: true,\n            };\n        case actions.READ_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case actions.READ_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n                loading: false,\n            };\n        case actions.READ_SUCCESS:\n            const payload = action.payload;\n            if (isJSONAPIErrorResponsePayload(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: payload,\n                    loading: false,\n                };\n            } else if (isApiError(payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: payload,\n                    loading: false,\n                }\n            } else {\n                return {\n                    ...state,\n                    lastReceived: new Date(),\n                    loading: false,\n                    organization: payload,\n                };\n            }\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/organization/types.ts",
    "content": "export interface Organization {\n    id?: string;\n    name: string;\n    payload_prefix: string;\n    x509_ou: string;\n    x509_o: string;\n    x509_st: string;\n    x509_c: string;\n}\n"
  },
  {
    "path": "ui/src/store/pki/actions.ts",
    "content": "import { RSAA, RSAAction } from \"redux-api-middleware\";\nimport { JSONAPI_HEADERS } from \"../constants\"\nimport {CertificatePurpose} from \"./types\";\nimport {RootState} from \"../../reducers\";\n\nexport type NEW_REQUEST = \"signing_requests/NEW_REQUEST\";\nexport const NEW_REQUEST: NEW_REQUEST = \"signing_requests/NEW_REQUEST\";\nexport type NEW_SUCCESS = \"signing_requests/NEW_SUCCESS\";\nexport const NEW_SUCCESS: NEW_SUCCESS = \"signing_requests/NEW_SUCCESS\";\nexport type NEW_FAILURE = \"signing_requests/NEW_FAILURE\";\nexport const NEW_FAILURE: NEW_FAILURE = \"signing_requests/NEW_FAILURE\";\n\nexport const newCertificateSigningRequest = (purpose: CertificatePurpose): RSAAction<NEW_REQUEST, NEW_SUCCESS, NEW_FAILURE>  => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify({ purpose }),\n            endpoint: \"/api/v1/certificate_signing_requests/new\",\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                NEW_REQUEST,\n                NEW_SUCCESS,\n                NEW_FAILURE,\n            ],\n        },\n    }\n};\n"
  },
  {
    "path": "ui/src/store/pki/types.ts",
    "content": "export type CertificatePurpose = \"apns\" | \"ssl\";\n"
  },
  {
    "path": "ui/src/store/profile/reducer.ts",
    "content": "import {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from \"../json-api\";\nimport {PatchRelationshipActionResponse, ReadActionResponse} from \"../profiles/actions\";\nimport {ProfilesActionTypes} from \"../profiles/actions\";\nimport {Profile} from \"../profiles/types\";\nimport {Tag} from \"../tags/types\";\n\nexport interface IProfileState {\n    profile?: JSONAPIDataObject<Profile>;\n    loading: boolean;\n    error: boolean;\n    errorDetail?: any\n    tags?: Array<JSONAPIDataObject<Tag>>;\n    tagsLoading: boolean;\n}\n\nconst initialState: IProfileState = {\n    error: false,\n    errorDetail: null,\n    loading: false,\n    profile: null,\n    tagsLoading: false,\n};\n\ntype ProfileAction = ReadActionResponse | PatchRelationshipActionResponse;\n\nexport function profile(state: IProfileState = initialState, action: ProfileAction): IProfileState {\n    switch (action.type) {\n        case ProfilesActionTypes.READ_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n\n        case ProfilesActionTypes.READ_FAILURE:\n            return {\n                ...state,\n                error: true,\n                errorDetail: action.payload,\n            };\n\n        case ProfilesActionTypes.READ_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                }\n            } else {\n                let tags: Array<JSONAPIDataObject<Tag>> = [];\n\n                if (action.payload.included) {\n                    tags = action.payload.included.filter((included: JSONAPIDataObject<any>) => (included.type === \"tags\"));\n                }\n\n                return {\n                    ...state,\n                    profile: action.payload.data,\n                    loading: false,\n                    tags,\n                };\n            }\n        case ProfilesActionTypes.REL_PATCH_REQUEST:\n            return {\n                ...state,\n                tagsLoading: true,\n            };\n        case ProfilesActionTypes.REL_PATCH_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                }\n            } else {\n                const profile: JSONAPIDataObject<Profile> = {\n                    ...state.profile,\n                    relationships: {\n                        ...state.profile.relationships,\n                        tags: action.payload.data.relationships.tags,\n                    },\n                };\n\n                return {\n                    ...state,\n                    profile,\n                    tagsLoading: false,\n                };\n            }\n\n        case ProfilesActionTypes.REL_PATCH_FAILURE:\n            return {\n                ...state,\n                tagsLoading: false,\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/profiles/actions.ts",
    "content": "import {Dispatch} from \"redux\";\nimport {ApiError, HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {ThunkAction} from \"redux-thunk\";\nimport {\n    JSONAPIRelationship, RSAAIndexActionRequest,\n    RSAAIndexActionResponse, RSAAReadActionRequest, RSAAReadActionResponse,\n} from \"../json-api\";\nimport {JSONAPIDetailResponse} from \"../json-api\";\nimport {Profile, ProfileRelationship} from \"./types\";\nimport {Tag} from \"../tags/types\";\nimport {RootState} from \"../../reducers/index\";\nimport {JSONAPI_HEADERS} from \"../constants\"\nimport {encodeJSONAPIIndexParameters, FlaskFilter, FlaskFilters} from \"../../flask-rest-jsonapi\";\n\nexport enum ProfilesActionTypes {\n    INDEX_REQUEST = \"profiles/INDEX_REQUEST\",\n    INDEX_SUCCESS = \"profiles/INDEX_SUCCESS\",\n    INDEX_FAILURE = \"profiles/INDEX_FAILURE\",\n    READ_REQUEST = \"profiles/READ_REQUEST\",\n    READ_SUCCESS = \"profiles/READ_SUCCESS\",\n    READ_FAILURE = \"profiles/READ_FAILURE\",\n    REL_PATCH_REQUEST = \"profiles/relationships/PATCH_REQUEST\",\n    REL_PATCH_SUCCESS = \"profiles/relationships/PATCH_SUCCESS\",\n    REL_PATCH_FAILURE = \"profiles/relationships/PATCH_FAILURE\",\n}\n\nexport type IndexActionRequest = RSAAIndexActionRequest<ProfilesActionTypes.INDEX_REQUEST, ProfilesActionTypes.INDEX_SUCCESS, ProfilesActionTypes.INDEX_FAILURE>;\nexport type IndexActionResponse = RSAAIndexActionResponse<ProfilesActionTypes.INDEX_REQUEST, ProfilesActionTypes.INDEX_SUCCESS, ProfilesActionTypes.INDEX_FAILURE, Profile>;\n\nexport const index = encodeJSONAPIIndexParameters((queryParameters: string[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/profiles?\" + queryParameters.join(\"&\"),\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                ProfilesActionTypes.INDEX_REQUEST,\n                ProfilesActionTypes.INDEX_SUCCESS,\n                ProfilesActionTypes.INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<ProfilesActionTypes.INDEX_REQUEST, ProfilesActionTypes.INDEX_SUCCESS, ProfilesActionTypes.INDEX_FAILURE>);\n});\n\nexport type ReadActionRequest = RSAAReadActionRequest<ProfilesActionTypes.READ_REQUEST, ProfilesActionTypes.READ_SUCCESS, ProfilesActionTypes.READ_FAILURE>;\nexport type ReadActionResponse = RSAAReadActionResponse<ProfilesActionTypes.READ_REQUEST, ProfilesActionTypes.READ_SUCCESS, ProfilesActionTypes.READ_FAILURE, JSONAPIDetailResponse<Profile, undefined>>;\n\nexport const read: ReadActionRequest = (id: string, include?: string[]) => {\n\n    let inclusions = \"\";\n    if (include && include.length) {\n        inclusions = \"include=\" + include.join(\",\")\n    }\n\n    return {\n        [RSAA]: {\n            endpoint: `/api/v1/profiles/${id}?${inclusions}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"GET\",\n            types: [\n                ProfilesActionTypes.READ_REQUEST,\n                ProfilesActionTypes.READ_SUCCESS,\n                ProfilesActionTypes.READ_FAILURE,\n            ],\n        },\n    }\n};\n\nexport type PatchRelationshipActionRequest = (parentId: string, relationship: ProfileRelationship, data: JSONAPIRelationship[]) => RSAAction<\n    ProfilesActionTypes.REL_PATCH_REQUEST, ProfilesActionTypes.REL_PATCH_SUCCESS, ProfilesActionTypes.REL_PATCH_FAILURE>;\nexport type PatchRelationshipActionResponse = RSAAReadActionResponse<\n    ProfilesActionTypes.REL_PATCH_REQUEST,\n    ProfilesActionTypes.REL_PATCH_SUCCESS,\n    ProfilesActionTypes.REL_PATCH_FAILURE,\n    JSONAPIDetailResponse<Profile, Tag>>;\n\nexport const patchRelationship: PatchRelationshipActionRequest = (parentId: string, relationship: ProfileRelationship, data: JSONAPIRelationship[]) => {\n    return {\n        [RSAA]: {\n            body: JSON.stringify({ data }),\n            endpoint: `/api/v1/profiles/${parentId}/relationships/${relationship}`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"PATCH\",\n            types: [\n                ProfilesActionTypes.REL_PATCH_REQUEST,\n                ProfilesActionTypes.REL_PATCH_SUCCESS,\n                ProfilesActionTypes.REL_PATCH_FAILURE,\n            ],\n        },\n    }\n};\n\nexport const UPLOAD = \"profiles/UPLOAD\";\nexport type UPLOAD = typeof UPLOAD;\n\nexport const UPLOAD_REQUEST = \"profiles/UPLOAD_REQUEST\";\nexport type UPLOAD_REQUEST = typeof UPLOAD_REQUEST;\nexport const UPLOAD_SUCCESS = \"profiles/UPLOAD_SUCCESS\";\nexport type UPLOAD_SUCCESS = typeof UPLOAD_SUCCESS;\nexport const UPLOAD_FAILURE = \"profiles/UPLOAD_FAILURE\";\nexport type UPLOAD_FAILURE = typeof UPLOAD_FAILURE;\n\nexport type UploadActionRequest = (file: File) => ThunkAction<void, RootState, void, UploadActionResponse>;\nexport type UploadActionResponse = RSAAReadActionResponse<UPLOAD_REQUEST, UPLOAD_SUCCESS, UPLOAD_FAILURE, JSONAPIDetailResponse<Profile, undefined>>;\n\nexport const upload = (file: File): ThunkAction<void, RootState, void, UploadActionResponse> => (\n    dispatch: Dispatch,\n    getState: () => RootState,\n    extraArgument: void) => {\n\n    const data = new FormData();\n    data.append(\"file\", file);\n    dispatch({\n        payload: data,\n        type: UPLOAD,\n    });\n\n    dispatch({\n        [RSAA]: {\n            body: data,\n            endpoint: `/api/v1/upload/profiles`,\n            method: \"POST\",\n            types: [\n                UPLOAD_REQUEST,\n                UPLOAD_SUCCESS,\n                UPLOAD_FAILURE,\n            ],\n        },\n    })\n};\n"
  },
  {
    "path": "ui/src/store/profiles/reducer.ts",
    "content": "import * as actions from \"./actions\";\nimport {IndexActionResponse, UploadActionResponse} from \"./actions\";\nimport {JSONAPIDataObject, isJSONAPIErrorResponsePayload} from \"../json-api\";\nimport {Profile} from \"./types\";\nimport {ApiError} from \"redux-api-middleware\";\nimport {isApiError} from \"../../guards\";\nimport {IResults, ResultsDefaultState} from \"../../reducers/interfaces\";\nimport {ProfilesActionTypes} from \"./actions\";\n\nexport interface ProfilesState extends IResults<Array<JSONAPIDataObject<Profile>>> {\n    pageProperties: any;\n    uploading: boolean;\n    uploadError: boolean;\n    uploadErrorDetail?: ApiError;\n}\n\nconst initialState: ProfilesState = {\n    ...ResultsDefaultState,\n    pageProperties: {\n        currentPage: 1,\n        pageSize: 10,\n        recordCount: 0,\n    },\n    uploadError: false,\n    uploadErrorDetail: null,\n    uploading: false,\n};\n\ntype ProfilesAction = IndexActionResponse | UploadActionResponse;\n\nexport function profiles(state: ProfilesState = initialState, action: ProfilesAction): ProfilesState {\n    switch (action.type) {\n        case ProfilesActionTypes.INDEX_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n\n        case ProfilesActionTypes.INDEX_FAILURE:\n            return {\n                ...state,\n                error: action.payload,\n            };\n\n        case ProfilesActionTypes.INDEX_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: action.payload,\n                    loading: false,\n                };\n            } else {\n                return {\n                    ...state,\n                    items: action.payload.data,\n                    lastReceived: new Date(),\n                    loading: false,\n                    recordCount: action.payload.meta.count,\n                };\n            }\n        case actions.UPLOAD_FAILURE:\n            if (isApiError(action.payload)) {\n                return {\n                    ...state,\n                    uploadError: true,\n                    uploadErrorDetail: action.payload,\n                    uploading: false,\n                };\n            }\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/profiles/types.ts",
    "content": "export interface Profile {\n    id?: number;\n    description?: string;\n    display_name?: string;\n    expiration_date?: Date;\n    identifier: string;\n    organization?: string;\n    uuid: string;\n    removal_disallowed?: boolean;\n    version: number;\n    scope?: string;\n    removal_date?: Date;\n    duration_until_removal?: number;\n    consent_en?: string;\n}\n\nexport type ProfileRelationship = \"tags\";\n"
  },
  {
    "path": "ui/src/store/redux-api-middleware.ts",
    "content": "// Standardised JSON-API Index ActionCreator Response (passed to reducer)\n\nimport {Action} from \"redux\";\nimport {ApiError, InvalidRSAA, RequestError, RSAAction} from \"redux-api-middleware\";\nimport {JSONAPIDataObject, JSONAPIErrorResponse, JSONAPIListResponse} from \"./json-api\";\nimport {JSONAPIDocument, Relationships} from \"../json-api-v1\";\nimport {FlaskFilters} from \"../flask-rest-jsonapi\";\n\n\nexport interface RSAARequestAction<TRequest> extends Action<TRequest> {\n    error?: boolean;\n    payload?: InvalidRSAA | RequestError;\n    type: TRequest;\n}\n\nexport interface RSAAFailureAction<TFailure> extends Action<TFailure> {\n    payload: ApiError;\n    type: TFailure;\n}\n\n// Success can still contain API errors that are 2xx responses\nexport interface RSAASuccessAction<TSuccess, TResponse> extends Action<TSuccess> {\n    payload: TResponse;\n    type: TSuccess;\n}\n\nexport type RSAAActionResponse<TRequest, TSuccess, TFailure, TData, TIncluded> =\n    RSAARequestAction<TRequest> |\n    RSAAFailureAction<TFailure> |\n    RSAASuccessAction<TSuccess, JSONAPIDocument<TData, TIncluded>>;\n\n// Standardised JSON-API Index ActionCreator\nexport type RSAAIndexActionCreator<TRequest, TSuccess, TFailure> =\n    (size?: number, pageNumber?: number, sort?: string[], filters?: FlaskFilters) =>\n        RSAAction<TRequest, TSuccess, TFailure>;\n\nexport type RSAAReadActionCreator<TRequest, TSuccess, TFailure> = (id: string, include?: string[])\n    => RSAAction<TRequest, TSuccess, TFailure>;\n\nexport type RSAAPostActionCreator<TRequest, TSuccess, TFailure, TValues> =\n    (values: TValues, relationships?: Relationships) =>\n    RSAAction<TRequest, TSuccess, TFailure>;\n\nexport type RSAAPatchActionCreator<TRequest, TSuccess, TFailure, TValues> = (id: string, values: Partial<TValues>)\n    => RSAAction<TRequest, TSuccess, TFailure>;\n"
  },
  {
    "path": "ui/src/store/table/actions.ts",
    "content": "import {ActionCreator} from \"redux\";\n\nexport enum TableActionTypes {\n    TOGGLE_SELECTION = \"@react_table/TOGGLE_SELECTION\",\n    TOGGLE_ALL = \"@react_table/TOGGLE_ALL\",\n}\n\nexport type ToggleSelectionActionCreator = (key: string, shiftKeyPressed: boolean, row: any) => IToggleSelectionAction;\nexport interface IToggleSelectionAction {\n    key: string;\n    shiftKeyPressed: boolean;\n    row: any;\n    type: TableActionTypes;\n}\n\nexport const toggleSelection: ActionCreator<IToggleSelectionAction> =\n    (key: string, shiftKeyPressed: boolean, row: any) => {\n    return {\n        key,\n        row,\n        shiftKeyPressed,\n        type: TableActionTypes.TOGGLE_SELECTION,\n    };\n};\n\nexport interface IToggleAllAction {\n    type: TableActionTypes;\n}\n\nexport const toggleAll: ActionCreator<IToggleAllAction> = () => {\n    return {\n        type: TableActionTypes.TOGGLE_ALL,\n    };\n};\n\nexport type TableActions = IToggleSelectionAction & IToggleAllAction;\n"
  },
  {
    "path": "ui/src/store/table/reducer.ts",
    "content": "import {Reducer} from \"redux\";\nimport {TableActions, TableActionTypes} from \"./actions\";\n\nexport interface ITableState {\n    pageSize: number;\n    pages: number;\n    selection: string[];\n}\n\nconst initialState: ITableState = {\n    pageSize: 20,\n    pages: 0,\n    selection: [],\n};\n\nexport const table: Reducer<ITableState, TableActions> = (state = initialState, action) => {\n    switch (action.type) {\n        case TableActionTypes.TOGGLE_ALL:\n            return state;\n        case TableActionTypes.TOGGLE_SELECTION:\n            let selection = [...state.selection];\n            const keyIndex = state.selection.indexOf(action.key);\n            if (keyIndex !== -1) {\n                selection = [\n                    ...selection.slice(0, keyIndex),\n                    ...selection.slice(keyIndex + 1),\n                ];\n            } else {\n                selection.push(action.key);\n            }\n\n            return {\n                ...state,\n                selection,\n            };\n        default:\n            return state;\n    }\n};\n"
  },
  {
    "path": "ui/src/store/table/types.ts",
    "content": "\nexport interface IReactTableState {\n    page: number;\n    pageSize: number;\n    filtered: Array<{ id: string; value: any; }>;\n    sorted: Array<{ id: string; desc: boolean; }>;\n}\n"
  },
  {
    "path": "ui/src/store/tags/actions.ts",
    "content": "import {Dispatch} from \"redux\";\nimport {HTTPVerb, RSAA, RSAAction} from \"redux-api-middleware\";\nimport {ThunkAction} from \"redux-thunk\";\nimport {\n    JSONAPIDataObject, JSONAPIListResponse,\n    RSAAIndexActionRequest,\n    RSAAIndexActionResponse, RSAAPostActionRequest, RSAAPostActionResponse, RSAAReadActionRequest,\n    RSAAReadActionResponse,\n} from \"../json-api\";\nimport {JSONAPIDetailResponse, JSONAPIErrorResponse} from \"../json-api\";\nimport {RootState} from \"../../reducers/index\";\nimport {JSON_HEADERS, JSONAPI_HEADERS} from \"../constants\"\nimport {Tag} from \"./types\";\nimport {\n    encodeJSONAPIChildIndexParameters,\n    encodeJSONAPIIndexParameters,\n    FlaskFilter,\n    FlaskFilters\n} from \"../../flask-rest-jsonapi\";\n\nexport enum TagsActionTypes {\n    INDEX_REQUEST = \"tags/INDEX_REQUEST\",\n    INDEX_SUCCESS = \"tags/INDEX_SUCCESS\",\n    INDEX_FAILURE = \"tags/INDEX_FAILURE\",\n\n    POST_REQUEST = \"tags/POST_REQUEST\",\n    POST_SUCCESS = \"tags/POST_SUCCESS\",\n    POST_FAILURE = \"tags/POST_FAILURE\",\n}\n\nexport type IndexActionRequest = RSAAIndexActionRequest<TagsActionTypes.INDEX_REQUEST, TagsActionTypes.INDEX_SUCCESS, TagsActionTypes.INDEX_FAILURE>;\nexport type IndexActionResponse = RSAAIndexActionResponse<TagsActionTypes.INDEX_REQUEST, TagsActionTypes.INDEX_SUCCESS, TagsActionTypes.INDEX_FAILURE, Tag>;\n\nexport const index = encodeJSONAPIIndexParameters((queryParameters: string[]) => {\n    return ({\n        [RSAA]: {\n            endpoint: \"/api/v1/tags?\" + queryParameters.join(\"&\"),\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: (\"GET\" as HTTPVerb),\n            types: [\n                TagsActionTypes.INDEX_REQUEST,\n                TagsActionTypes.INDEX_SUCCESS,\n                TagsActionTypes.INDEX_FAILURE,\n            ],\n        },\n    } as RSAAction<TagsActionTypes.INDEX_REQUEST, TagsActionTypes.INDEX_SUCCESS, TagsActionTypes.INDEX_FAILURE>);\n});\n\nexport type PostActionRequest = RSAAPostActionRequest<TagsActionTypes.POST_REQUEST, TagsActionTypes.POST_SUCCESS, TagsActionTypes.POST_FAILURE, Tag>;\nexport type PostActionResponse = RSAAPostActionResponse<TagsActionTypes.POST_REQUEST, TagsActionTypes.POST_SUCCESS, TagsActionTypes.POST_FAILURE, JSONAPIDetailResponse<Tag, undefined>>;\n\nexport const post: PostActionRequest = (values: Tag) => {\n\n    return ({\n        [RSAA]: {\n            body: JSON.stringify({\n                data: {\n                    attributes: values,\n                    type: \"tags\",\n                },\n            }),\n            endpoint: `/api/v1/tags`,\n            headers: (state: RootState) => ({\n                ...JSONAPI_HEADERS,\n                Authorization: `Bearer ${state.auth.access_token}`,\n            }),\n            method: \"POST\",\n            types: [\n                TagsActionTypes.POST_REQUEST,\n                TagsActionTypes.POST_SUCCESS,\n                TagsActionTypes.POST_FAILURE,\n            ],\n        },\n    } as RSAAction<TagsActionTypes.POST_REQUEST, TagsActionTypes.POST_SUCCESS, TagsActionTypes.POST_FAILURE>);\n};\n\nexport type CreateAndApplyRequest = (values: Tag) => ThunkAction<void, RootState, void, PostActionResponse>;\n\nexport const createAndApply: CreateAndApplyRequest = (values) => (dispatch: Dispatch, getState) => {\n    dispatch(post(values));\n};\n"
  },
  {
    "path": "ui/src/store/tags/reducer.ts",
    "content": "import {isJSONAPIErrorResponsePayload, JSONAPIDataObject, JSONAPIErrorResponse} from \"../json-api\";\nimport {Tag} from \"./types\";\nimport {IndexActionResponse, TagsActionTypes} from \"./actions\";\n\nexport interface ITagsState {\n    loading: boolean;\n    items: Array<JSONAPIDataObject<Tag>>;\n    error: boolean;\n    errorDetail?: JSONAPIErrorResponse;\n}\n\nconst initialState: ITagsState = {\n    error: false,\n    items: [],\n    loading: false,\n};\n\ntype TagsAction = IndexActionResponse;\n\nexport function tags(state: ITagsState = initialState, action: TagsAction): ITagsState {\n    switch (action.type) {\n        case TagsActionTypes.INDEX_REQUEST:\n            return {\n                ...state,\n                loading: true,\n            };\n        case TagsActionTypes.INDEX_SUCCESS:\n            if (isJSONAPIErrorResponsePayload(action.payload)) {\n                return {\n                    ...state,\n                    error: true,\n                    errorDetail: action.payload,\n                }\n            } else {\n                return {\n                    ...state,\n                    error: false,\n                    items: action.payload.data,\n                    loading: false,\n                };\n            }\n        case TagsActionTypes.INDEX_FAILURE:\n            return {\n                ...state,\n                error: true,\n                loading: false,\n            };\n        default:\n            return state;\n    }\n}\n"
  },
  {
    "path": "ui/src/store/tags/types.ts",
    "content": "export interface Tag {\n    id?: string;\n    name: string;\n    color: string;\n}\n"
  },
  {
    "path": "ui/src/stories/DEPProfileForm.tsx",
    "content": "import { action } from \"@storybook/addon-actions\";\nimport { storiesOf } from \"@storybook/react\";\nimport * as React from \"react\";\nimport {Container} from \"semantic-ui-react\";\nimport {DEPProfileForm} from \"../components/forms/DEPProfileForm\";\n\nstoriesOf(\"DEPProfileForm\", module)\n    .add(\"default\", () => (\n        <Container>\n            <DEPProfileForm\n                loading={false}\n                onSubmit={action(\"onSubmit\")}\n                activeIndex={1}\n                onClickAccordionTitle={action(\"onClickTitle\")}\n            />\n        </Container>\n    ));\n"
  },
  {
    "path": "ui/src/stories/index.ts",
    "content": "import \"./redux\";\n\nexport default {}\n"
  },
  {
    "path": "ui/src/stories/redux.tsx",
    "content": "import * as React from 'react';\nimport {Provider} from 'react-redux';\nimport {combineReducers, compose, createStore} from 'redux';\nimport {addDecorator, Story, StoryDecorator} from '@storybook/react';\n\nconst composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;\n\nconst store = createStore((state, action) => state, composeEnhancers());\n\nconst StoreDecorator: StoryDecorator = (story) => (\n    <Provider store={store}>\n        { story() }\n    </Provider>\n);\n\naddDecorator(StoreDecorator);\n"
  },
  {
    "path": "ui/tsconfig.json",
    "content": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"noImplicitAny\": true,\n    \"jsx\": \"react\",\n    \"allowJs\": false,\n    \"noEmitOnError\": false,\n    \"pretty\": true,\n    \"removeComments\": true,\n    \"target\": \"ES5\"\n  },\n  \"include\": [\n    \"./src/**/*\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"_deprecated\"\n  ],\n  \"paths\": {\n    \"lodash/*\": [\n      \"node_modules/@types/lodash-es/*\"\n    ]\n  }\n}\n"
  },
  {
    "path": "ui/tslint.json",
    "content": "{\n    \"defaultSeverity\": \"error\",\n    \"extends\": [\n        \"tslint:recommended\"\n    ],\n    \"jsRules\": {},\n    \"rules\": {\n        \"semicolon\": false\n    },\n    \"rulesDirectory\": []\n}"
  },
  {
    "path": "ui/webpack.config.hmr.js",
    "content": "const path = require('path');\nconst fs = require('fs');\nconst webpack = require('webpack');\nconst {CheckerPlugin} = require('awesome-typescript-loader');\nconst BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;\n\nmodule.exports = {\n  entry: {\n    app: [\n      'webpack-dev-server/client?https://localhost:4000',\n      'webpack/hot/only-dev-server',\n      './src/entry.tsx'\n    ]\n  },\n  mode: 'development',\n  output: {\n    path: path.resolve(__dirname, \"..\", \"commandment\", \"static\"),\n    filename: 'app.js',\n    publicPath: 'https://localhost:4000/static/'\n  },\n\n  resolve: {\n    extensions: ['.ts', '.tsx', '.js', '.jsx'],\n    alias: { // avoid lodash duplication: https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/\n      'lodash-es': 'lodash',\n      'lodash.get': 'lodash/get',\n      'lodash.isfunction': 'lodash/isFunction',\n      'lodash.isobject': 'lodash/isObject',\n      'lodash.merge': 'lodash/merge',\n      'lodash.reduce': 'lodash/reduce',\n      'lodash.set': 'lodash/set',\n      'lodash.unset': 'lodash/unset'\n    }\n  },\n\n  devtool: 'cheap-module-eval-source-map',\n  target: 'web',\n\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        use: ['awesome-typescript-loader'] // 'react-hot-loader/webpack',\n      },\n      {\n        test: /\\.js$/,\n        include: [\n          path.resolve(__dirname, \"node_modules/semantic-ui-react\"),\n          path.resolve(__dirname, \"node_modules/byte-size\")\n        ],\n        loader: \"babel-loader\"\n      },\n      {\n        test: /\\.scss$/,\n        use: [{\n          loader: 'style-loader'\n        }, {\n          loader: 'css-loader'\n        }, {\n          loader: 'resolve-url-loader'\n        }, {\n          loader: 'sass-loader',\n          options: {\n            sourceMap: true\n          }\n        }]\n      },\n      {\n        test: /\\.(png|jpg|svg|gif)$/,\n        use: [{\n          loader: 'url-loader',\n          options: {\n            limit: 10000,\n            name: '[name]-[hash].[ext]'\n          }\n        }]\n      },\n      {\n        test: /\\.(ttf|eot|woff|svg|woff2)$/,\n        use: [{\n          loader: 'file-loader',\n          options: {\n            outputPath: 'fonts/'\n          }\n        }]\n      }\n    ]\n  },\n  plugins: [\n    new webpack.IgnorePlugin(/^\\.\\/locale$/, /moment$/), // Reduces size by not including all locales\n    new webpack.HotModuleReplacementPlugin(),\n    new CheckerPlugin(),\n    new BundleAnalyzerPlugin(),\n  ],\n\n  devServer: {\n    // This must be a full hostname for HMR to work\n    publicPath: \"https://localhost:4000/static/\",\n    hot: true,\n    port: 4000,\n    disableHostCheck: true,\n    https: {\n      key: fs.readFileSync('../ssl/server.key'),\n      cert: fs.readFileSync('../ssl/server.crt'),\n      ca: fs.readFileSync('../ssl/ca.crt')\n    },\n    headers: {\n      'Access-Control-Allow-Origin': '*'\n    }\n  }\n};"
  },
  {
    "path": "ui/webpack.config.js",
    "content": "if (process.env.NODE_ENV === \"production\") {\n  console.log('using config from ./webpack.config.prod');\n  module.exports = require('./webpack.config.prod');\n} else {\n  console.log('using config from ./webpack.config.hmr');\n  module.exports = require('./webpack.config.hmr');\n}\n"
  },
  {
    "path": "ui/webpack.config.prod.js",
    "content": "const path = require('path');\nconst webpack = require('webpack');\nconst ExtractTextPlugin = require(\"extract-text-webpack-plugin\");\nconst {CheckerPlugin} = require('awesome-typescript-loader');\nconst {createLodashTransformer} = require('typescript-plugin-lodash');\nconst lodashTransformer = createLodashTransformer();\n\nconst extractSass = new ExtractTextPlugin({\n  filename: \"css/[name].css\"\n});\n\nmodule.exports = {\n  entry: {\n    app: [\n      './src/entry.tsx'\n    ]\n  },\n  mode: 'production',\n  output: {\n    path: path.resolve(__dirname, \"..\", \"commandment\", \"static\"),\n    filename: 'app.js',\n    publicPath: '/static/'\n  },\n\n  resolve: {\n    extensions: ['.ts', '.tsx', '.js', '.jsx'],\n    alias: { // avoid lodash duplication: https://www.contentful.com/blog/2017/10/27/put-your-webpack-bundle-on-a-diet-part-3/\n      'lodash-es': 'lodash',\n      'lodash.get': 'lodash/get',\n      'lodash.isfunction': 'lodash/isFunction',\n      'lodash.isobject': 'lodash/isObject',\n      'lodash.merge': 'lodash/merge',\n      'lodash.reduce': 'lodash/reduce',\n      'lodash.set': 'lodash/set',\n      'lodash.unset': 'lodash/unset'\n    }\n  },\n  target: 'web',\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        use: [{\n          loader: 'awesome-typescript-loader',\n          options: {\n            errorsAsWarnings: true\n          }\n        }]\n      },\n      {\n        test: /\\.js$/,\n        include: [\n          path.resolve(__dirname, \"node_modules/semantic-ui-react\"),\n          path.resolve(__dirname, \"node_modules/byte-size\")\n        ],\n        loader: \"babel-loader\"\n      },\n      {\n        test: /\\.scss$/,\n        use: extractSass.extract({\n          use: [{\n            loader: 'css-loader'\n          }, {\n            loader: 'resolve-url-loader'\n          }, {\n            loader: 'sass-loader?sourceMap'\n          }],\n          fallback: 'style-loader'\n        })\n      },\n      {\n        test: /\\.css$/,\n        use: extractSass.extract({\n          use: [{\n            loader: 'css-loader'\n          }, {\n            loader: 'resolve-url-loader'\n          }]\n        })\n      },\n      {\n        test: /\\.(png|jpg|svg|gif)$/,\n        use: [{\n          loader: 'url-loader',\n          options: {\n            limit: 10000,\n            name: '[name]-[hash].[ext]',\n            outputPath: 'images/'\n          }\n        }]\n      },\n      {\n        test: /\\.(ttf|eot|svg|woff|woff2)$/,\n        use: [{\n          loader: 'file-loader',\n          options: {\n            outputPath: 'fonts/'\n          }\n        }]\n      }\n    ]\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env.NODE_ENV': JSON.stringify('production')\n    }),\n    extractSass,\n    new CheckerPlugin(),\n    new webpack.IgnorePlugin(/^\\.\\/locale$/, /moment$/),\n  ]\n};"
  }
]