├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── account-config ├── README.md └── template.yaml ├── apis ├── contributors │ ├── ContributorsAPI.md │ ├── src │ │ ├── create_api_token │ │ │ ├── index.py │ │ │ ├── requirements.txt │ │ │ └── schemas │ │ │ │ └── token.json │ │ ├── get_contributors │ │ │ ├── index.py │ │ │ └── requirements.txt │ │ ├── implicit_login │ │ │ ├── index.py │ │ │ └── requirements.txt │ │ ├── invalidate_api_token │ │ │ ├── index.py │ │ │ └── requirements.txt │ │ └── oauth2_appleid_token │ │ │ ├── index.py │ │ │ └── requirements.txt │ └── template.yaml ├── jamf │ ├── src │ │ └── read_titles │ │ │ ├── index.py │ │ │ └── requirements.txt │ └── template.yaml └── titles │ ├── TitlesAPI.md │ ├── src │ ├── authorizer │ │ ├── index.py │ │ └── requirements.txt │ ├── create_title │ │ ├── index.py │ │ ├── requirements.txt │ │ └── schemas │ │ │ └── full_definition.json │ ├── delete_title │ │ ├── index.py │ │ └── requirements.txt │ ├── read_titles │ │ ├── index.py │ │ └── requirements.txt │ └── update_title_version │ │ ├── index.py │ │ ├── requirements.txt │ │ └── schemas │ │ └── version.json │ └── template.yaml ├── buildspec.yml ├── docs └── JamfProSetup.md ├── images ├── AppleID-Sign-In.png ├── CommunityPatch-Architecture.png ├── Postman-Collection.png └── Tokens-Page.png ├── pipeline.yaml ├── postman-collections ├── CommunityPatch.Dev.postman_collection.json ├── CommunityPatch.Dev.postman_environment.json └── Postman.md ├── resources ├── global │ ├── cognito.yaml │ └── tables.yaml └── regional │ ├── src │ ├── dynamodb_stream_arn_lookup │ │ ├── index.py │ │ └── requirements.txt │ └── stream_processor │ │ ├── index.py │ │ └── requirements.txt │ └── template.yaml ├── src └── layers │ ├── api_shared │ ├── api_helpers.py │ └── requirements.txt │ └── security_shared │ ├── requirements.txt │ └── security_helpers.py ├── template.yaml └── web ├── content ├── css │ ├── custom.css │ └── dataTables.conditionalPaging.js ├── images │ └── favicon │ │ ├── 114x114.png │ │ ├── 120x120.png │ │ ├── 144x144.png │ │ ├── 150x150.png │ │ ├── 152x152.png │ │ ├── 16x16.png │ │ ├── 180x180.png │ │ ├── 192x192.png │ │ ├── 310x310.png │ │ ├── 32x32.png │ │ ├── 36x36.png │ │ ├── 48x48.png │ │ ├── 57x57.png │ │ ├── 60x60.png │ │ ├── 70x70.png │ │ ├── 72x72.png │ │ ├── 76x76.png │ │ ├── 96x96.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon.ico │ │ └── manifest.json ├── index.html └── js │ └── custom.js └── template.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.aws-sam/ 2 | notes.rst 3 | docs/_build/ 4 | *samconfig.toml 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bryson Tyrrell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [packages] 7 | pyjwt = "*" 8 | jsonschema = "*" 9 | "jinja2" = "*" 10 | "boto3" = "*" 11 | cryptography = "*" 12 | aws-xray-sdk = "*" 13 | 14 | [dev-packages] 15 | sphinx = "*" 16 | sphinx-rtd-theme = "*" 17 | wheel = "*" 18 | 19 | [requires] 20 | python_version = "3.7" 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "9b65b91c8ed1c8e34fabf68af67830bc468d0ddd955ab8adaa6c6fe7503fb4e8" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "attrs": { 20 | "hashes": [ 21 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 22 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 23 | ], 24 | "version": "==19.3.0" 25 | }, 26 | "aws-xray-sdk": { 27 | "hashes": [ 28 | "sha256:263a38f3920d9dc625e3acb92e6f6d300f4250b70f538bd009ce6e485676ab74", 29 | "sha256:612dba6efc3704ef224ac0747b05488b8aad94e71be3ece4edbc051189d50482" 30 | ], 31 | "index": "pypi", 32 | "version": "==2.4.3" 33 | }, 34 | "boto3": { 35 | "hashes": [ 36 | "sha256:2eb7dd99d92bb094a1fcc5eb50162b304977f238c25557bfee98e25cba18ffc1", 37 | "sha256:d9406222f2442171e3a8a8da6e3b0cc780eed6ca835832bb993726d21914708b" 38 | ], 39 | "index": "pypi", 40 | "version": "==1.12.15" 41 | }, 42 | "botocore": { 43 | "hashes": [ 44 | "sha256:0347c44d3913ed0a818f83e0c317120f07c150a63ca4167ad3b4b55240a26f42", 45 | "sha256:857ec0be8aabd3591b0cca382bce723c0802d48ac0f0e8798ce305af93112a24" 46 | ], 47 | "version": "==1.15.15" 48 | }, 49 | "cffi": { 50 | "hashes": [ 51 | "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", 52 | "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", 53 | "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", 54 | "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", 55 | "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", 56 | "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", 57 | "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", 58 | "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", 59 | "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", 60 | "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", 61 | "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", 62 | "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", 63 | "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", 64 | "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", 65 | "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", 66 | "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", 67 | "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", 68 | "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", 69 | "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", 70 | "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", 71 | "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", 72 | "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", 73 | "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", 74 | "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", 75 | "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", 76 | "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", 77 | "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", 78 | "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" 79 | ], 80 | "version": "==1.14.0" 81 | }, 82 | "cryptography": { 83 | "hashes": [ 84 | "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", 85 | "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", 86 | "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", 87 | "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", 88 | "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", 89 | "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", 90 | "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", 91 | "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", 92 | "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", 93 | "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", 94 | "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", 95 | "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", 96 | "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", 97 | "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", 98 | "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", 99 | "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", 100 | "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", 101 | "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", 102 | "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", 103 | "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", 104 | "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" 105 | ], 106 | "index": "pypi", 107 | "version": "==2.8" 108 | }, 109 | "docutils": { 110 | "hashes": [ 111 | "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", 112 | "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", 113 | "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" 114 | ], 115 | "version": "==0.15.2" 116 | }, 117 | "future": { 118 | "hashes": [ 119 | "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" 120 | ], 121 | "version": "==0.18.2" 122 | }, 123 | "importlib-metadata": { 124 | "hashes": [ 125 | "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", 126 | "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" 127 | ], 128 | "markers": "python_version < '3.8'", 129 | "version": "==1.5.0" 130 | }, 131 | "jinja2": { 132 | "hashes": [ 133 | "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", 134 | "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" 135 | ], 136 | "index": "pypi", 137 | "version": "==2.11.1" 138 | }, 139 | "jmespath": { 140 | "hashes": [ 141 | "sha256:695cb76fa78a10663425d5b73ddc5714eb711157e52704d69be03b1a02ba4fec", 142 | "sha256:cca55c8d153173e21baa59983015ad0daf603f9cb799904ff057bfb8ff8dc2d9" 143 | ], 144 | "version": "==0.9.5" 145 | }, 146 | "jsonpickle": { 147 | "hashes": [ 148 | "sha256:71bca2b80ae28af4e3f86629ef247100af7f97032b5ca8d791c1f8725b411d95", 149 | "sha256:efc6839cb341985f0c24f98650a4c1063a2877c236ffd3d7e1662f0c482bac93" 150 | ], 151 | "version": "==1.3" 152 | }, 153 | "jsonschema": { 154 | "hashes": [ 155 | "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", 156 | "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" 157 | ], 158 | "index": "pypi", 159 | "version": "==3.2.0" 160 | }, 161 | "markupsafe": { 162 | "hashes": [ 163 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 164 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 165 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 166 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 167 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 168 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 169 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 170 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 171 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 172 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 173 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 174 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 175 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 176 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 177 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 178 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 179 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 180 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 181 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 182 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 183 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 184 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 185 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 186 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 187 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 188 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 189 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 190 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 191 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 192 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 193 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 194 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 195 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 196 | ], 197 | "version": "==1.1.1" 198 | }, 199 | "pycparser": { 200 | "hashes": [ 201 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 202 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 203 | ], 204 | "version": "==2.20" 205 | }, 206 | "pyjwt": { 207 | "hashes": [ 208 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 209 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 210 | ], 211 | "index": "pypi", 212 | "version": "==1.7.1" 213 | }, 214 | "pyrsistent": { 215 | "hashes": [ 216 | "sha256:cdc7b5e3ed77bed61270a47d35434a30617b9becdf2478af76ad2c6ade307280" 217 | ], 218 | "version": "==0.15.7" 219 | }, 220 | "python-dateutil": { 221 | "hashes": [ 222 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 223 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 224 | ], 225 | "version": "==2.8.1" 226 | }, 227 | "s3transfer": { 228 | "hashes": [ 229 | "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13", 230 | "sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db" 231 | ], 232 | "version": "==0.3.3" 233 | }, 234 | "six": { 235 | "hashes": [ 236 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 237 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 238 | ], 239 | "version": "==1.14.0" 240 | }, 241 | "urllib3": { 242 | "hashes": [ 243 | "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", 244 | "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" 245 | ], 246 | "markers": "python_version != '3.4'", 247 | "version": "==1.25.8" 248 | }, 249 | "wrapt": { 250 | "hashes": [ 251 | "sha256:0ec40d9fd4ec9f9e3ff9bdd12dbd3535f4085949f4db93025089d7a673ea94e8" 252 | ], 253 | "version": "==1.12.0" 254 | }, 255 | "zipp": { 256 | "hashes": [ 257 | "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", 258 | "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" 259 | ], 260 | "version": "==3.1.0" 261 | } 262 | }, 263 | "develop": { 264 | "alabaster": { 265 | "hashes": [ 266 | "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", 267 | "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" 268 | ], 269 | "version": "==0.7.12" 270 | }, 271 | "babel": { 272 | "hashes": [ 273 | "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", 274 | "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" 275 | ], 276 | "version": "==2.8.0" 277 | }, 278 | "certifi": { 279 | "hashes": [ 280 | "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", 281 | "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" 282 | ], 283 | "version": "==2019.11.28" 284 | }, 285 | "chardet": { 286 | "hashes": [ 287 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 288 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 289 | ], 290 | "version": "==3.0.4" 291 | }, 292 | "docutils": { 293 | "hashes": [ 294 | "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", 295 | "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", 296 | "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" 297 | ], 298 | "version": "==0.15.2" 299 | }, 300 | "idna": { 301 | "hashes": [ 302 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 303 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 304 | ], 305 | "version": "==2.9" 306 | }, 307 | "imagesize": { 308 | "hashes": [ 309 | "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", 310 | "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" 311 | ], 312 | "version": "==1.2.0" 313 | }, 314 | "jinja2": { 315 | "hashes": [ 316 | "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", 317 | "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" 318 | ], 319 | "index": "pypi", 320 | "version": "==2.11.1" 321 | }, 322 | "markupsafe": { 323 | "hashes": [ 324 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 325 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 326 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 327 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 328 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 329 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 330 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 331 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 332 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 333 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 334 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 335 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 336 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 337 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 338 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 339 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 340 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 341 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 342 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 343 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 344 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 345 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 346 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 347 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 348 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 349 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 350 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 351 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 352 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 353 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 354 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 355 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 356 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 357 | ], 358 | "version": "==1.1.1" 359 | }, 360 | "packaging": { 361 | "hashes": [ 362 | "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", 363 | "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" 364 | ], 365 | "version": "==20.3" 366 | }, 367 | "pygments": { 368 | "hashes": [ 369 | "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", 370 | "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" 371 | ], 372 | "version": "==2.5.2" 373 | }, 374 | "pyparsing": { 375 | "hashes": [ 376 | "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", 377 | "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" 378 | ], 379 | "version": "==2.4.6" 380 | }, 381 | "pytz": { 382 | "hashes": [ 383 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 384 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 385 | ], 386 | "version": "==2019.3" 387 | }, 388 | "requests": { 389 | "hashes": [ 390 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 391 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 392 | ], 393 | "version": "==2.23.0" 394 | }, 395 | "six": { 396 | "hashes": [ 397 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 398 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 399 | ], 400 | "version": "==1.14.0" 401 | }, 402 | "snowballstemmer": { 403 | "hashes": [ 404 | "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", 405 | "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" 406 | ], 407 | "version": "==2.0.0" 408 | }, 409 | "sphinx": { 410 | "hashes": [ 411 | "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66", 412 | "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb" 413 | ], 414 | "index": "pypi", 415 | "version": "==2.4.4" 416 | }, 417 | "sphinx-rtd-theme": { 418 | "hashes": [ 419 | "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4", 420 | "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a" 421 | ], 422 | "index": "pypi", 423 | "version": "==0.4.3" 424 | }, 425 | "sphinxcontrib-applehelp": { 426 | "hashes": [ 427 | "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", 428 | "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" 429 | ], 430 | "version": "==1.0.2" 431 | }, 432 | "sphinxcontrib-devhelp": { 433 | "hashes": [ 434 | "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", 435 | "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" 436 | ], 437 | "version": "==1.0.2" 438 | }, 439 | "sphinxcontrib-htmlhelp": { 440 | "hashes": [ 441 | "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", 442 | "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" 443 | ], 444 | "version": "==1.0.3" 445 | }, 446 | "sphinxcontrib-jsmath": { 447 | "hashes": [ 448 | "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", 449 | "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" 450 | ], 451 | "version": "==1.0.1" 452 | }, 453 | "sphinxcontrib-qthelp": { 454 | "hashes": [ 455 | "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", 456 | "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" 457 | ], 458 | "version": "==1.0.3" 459 | }, 460 | "sphinxcontrib-serializinghtml": { 461 | "hashes": [ 462 | "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", 463 | "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" 464 | ], 465 | "version": "==1.1.4" 466 | }, 467 | "urllib3": { 468 | "hashes": [ 469 | "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", 470 | "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" 471 | ], 472 | "markers": "python_version != '3.4'", 473 | "version": "==1.25.8" 474 | }, 475 | "wheel": { 476 | "hashes": [ 477 | "sha256:8788e9155fe14f54164c1b9eb0a319d98ef02c160725587ad60f14ddc57b6f96", 478 | "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e" 479 | ], 480 | "index": "pypi", 481 | "version": "==0.34.2" 482 | } 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommunityPatch 2 | 3 | CommunityPatch is a free, open-source, external patch source for Jamf Pro administrators to publish patch definitions they maintain for the broader Jamf community to subscribe to. Access CommunityPatch using an Apple ID, and then create and manage API tokens to interact with the service API. 4 | 5 | ### Quick Links 6 | 7 | [Jamf Pro Setup](docs/JamfProSetup.md) | [Contributors API](apis/contributors/ContributorsAPI.md) | [Titles API](apis/titles/TitlesAPI.md) 8 | 9 | ## Jamf Pro Setup 10 | 11 | Anyone can subscribed to a contributor's patch titles in Jamf Pro - they are publicly available feeds. All that is required is their `Contributor ID` or their `Contributor Alias`. 12 | 13 | See the [Jamf Pro Setup documentation](docs/JamfProSetup.md) for more details. 14 | 15 | ### Browsing Contributors 16 | 17 | _Coming soon..._ 18 | 19 | ### Searching Titles 20 | 21 | _Coming soon..._ 22 | 23 | ## Contributor QuickStart Guide 24 | 25 | If you wish to create and maintain a feed of patch title definitions on CommunityPatch, use the following instructions to sign in for the first time and begin using the APIs. 26 | 27 | ### Obtain an Access Token 28 | 29 | Sign into CommunityPatch using an Apple ID by navigating to https://contributors.communitypatch.dev/login and authenticating (you may use any Apple ID you wish as a part of this process). 30 | 31 | ![Apple ID Sign In](images/AppleID-Sign-In.png) 32 | 33 | At this time, CommunityPatch does not have a web UI. There is a landing page at the root domain that will render the generated `Access Token` for you to copy. This token is not accessible from the service. _At this time the **ID Token** is unused._ 34 | 35 | ![Apple ID Sign In](images/Tokens-Page.png) 36 | 37 | ### Explore With Postman 38 | 39 | A Postman environment and collection have been provided with this repo to help you explore the API and perform some of the tasks described below without having to write any code. See the [instructions here](postman-collections/Postman.md) to learn more. 40 | 41 | ### Creating API Tokens 42 | 43 | Your `Access Token` grants access to the Contributors API. This API allows you to manage your profile and `API Tokens`. API tokens can be created for use in scripting or other automation with CommunityPatch. These tokens can be configured with an expiration of up to 1 year, scoped to all or an individual title ID, 44 | 45 | See the [Contributors API documentation](apis/contributors/ContributorsAPI.md) for more details. 46 | 47 | ### Manage Titles 48 | 49 | Once you have created an API token it can be used to create and manage patch title definitions on the Titles API. 50 | 51 | See the [Titles API documentation](apis/titles/TitlesAPI.md) for more details. 52 | -------------------------------------------------------------------------------- /account-config/README.md: -------------------------------------------------------------------------------- 1 | # Account Configuration 2 | 3 | Deploy the template using a StackSet targeting: 4 | - us-east-1 5 | - us-east-2 6 | - eu-central-1 7 | - ap-southeast-2 8 | 9 | Ensure the `AWSCloudFormationStackSetAdministrationRole` and `AWSCloudFormationStackSetExecutionRole` IAM roles have been deployed in the account. See [this AWS guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html) for more information. 10 | -------------------------------------------------------------------------------- /account-config/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | 5 | Namespace: 6 | Type: String 7 | Description: Segments CloudFormation stack names and Parameter Store paths 8 | 9 | Resources: 10 | 11 | ArtifactBucket: 12 | Type: AWS::S3::Bucket 13 | Properties: 14 | AccessControl: BucketOwnerFullControl 15 | BucketName: !Sub '${Namespace}-communitypatch-${AWS::Region}-artifacts' 16 | LifecycleConfiguration: 17 | Rules: 18 | - Id: 14 day Lifecycle 19 | Status: Enabled 20 | ExpirationInDays: 14 21 | NoncurrentVersionExpirationInDays: 21 22 | VersioningConfiguration: 23 | Status: Enabled 24 | -------------------------------------------------------------------------------- /apis/contributors/ContributorsAPI.md: -------------------------------------------------------------------------------- 1 | # Contributors API 2 | 3 | The Contributors API provides the abiltiy to manage your profile and API tokens. 4 | 5 | ### Quick Links 6 | 7 | [Main Page](../../README.md) | [Jamf Pro Setup](../../docs/JamfProSetup.md) | [Titles API](../titles/TitlesAPI.md) 8 | 9 | ## Contributors 10 | 11 | ### GET /v1/contributors 12 | 13 | Return a list of all contributors. `No Authentication` 14 | 15 | #### Request 16 | 17 | n/a 18 | 19 | #### Response 20 | 21 | n/a 22 | 23 | ## API Token Management 24 | 25 | These endpoints require an `Access Token`. These tokens are obtained after signing in with an Apple ID (see the main page for more details). 26 | 27 | ### POST /v1/tokens 28 | 29 | Create an API token. 30 | 31 | #### Request 32 | 33 | | JSON Key | Description | Allowed Values | Required/Optional | 34 | |-----|-------------|----------------|-------------------| 35 | | expires_in_days | Set an expiration for the API token in days. | Integer: 1-365 (365 is default if not provided) | Optional | 36 | | titles_in_scope | Pass an array of existing Title IDs that the token will be allowed permissions to modify. If this value is set, the API token will be rejected for any Titles that are not a part of the scope. | Array of Strings: must be valid Title ID(s) for your Contributor ID. The default scope allows permissions to all Titles. | Optional 37 | 38 | ``` 39 | POST /v1/tokens 40 | Authorization: <> 41 | Content-Type: application-json 42 | 43 | { 44 | "expires_in_days": 365 45 | "titles_in_scope": ["<>"] 46 | } 47 | ``` 48 | 49 | #### Response 50 | 51 | | JSON Key | Description | 52 | |-----|-------------| 53 | | id | The token's unique ID. | 54 | | api_token | The API token. 55 | 56 | ``` 57 | 201 Created 58 | Content-Type: application/json 59 | 60 | { 61 | "id": "8c118ac0-daeb-4710-ab68-54d8b73eb441", 62 | "api_token": "eyJ0eXQk3e-No6QQqAXCRkwaiLEOUQP1m-No6QQqAXCRkwai..." 63 | } 64 | ``` 65 | 66 | #### POST /v1/tokens/{token_id}/invalidate 67 | 68 | 69 | Invalidate an API token. This action will revoke the token's access to the CommunityPatch APIs. _**This action cannot be reversed!**_ 70 | 71 | #### Request 72 | 73 | | Path Parameter | Description | 74 | |-----|-------------| 75 | | token_id | The API token ID that will be invalidated. | 76 | 77 | ``` 78 | POST /v1/tokens/{token_id}/invalidate 79 | Authorization: <<Access Token>> 80 | ``` 81 | 82 | #### Response 83 | 84 | ``` 85 | 204 No Content 86 | ``` 87 | -------------------------------------------------------------------------------- /apis/contributors/src/create_api_token/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import secrets 5 | import time 6 | import uuid 7 | 8 | import boto3 9 | from botocore.exceptions import ClientError 10 | from jsonschema import validate, ValidationError 11 | import jwt 12 | 13 | logger = logging.getLogger() 14 | logger.setLevel(logging.INFO) 15 | 16 | 17 | def read_schema(schema): 18 | with open(f"schemas/{schema}.json", "r") as f_obj: 19 | return json.load(f_obj) 20 | 21 | 22 | DOMAIN_NAME = os.getenv("DOMAIN_NAME") 23 | 24 | schema_definition = read_schema("token") 25 | 26 | communitypatchtable = boto3.resource("dynamodb").Table( 27 | os.getenv("COMMUNITY_PATCH_TABLE") 28 | ) 29 | 30 | 31 | def lambda_handler(event, context): 32 | """JSON payload values are optional. 33 | 34 | Expiration time limited/defaults to 1 year (60 * 60 * 24 * 365) 35 | Title scope defaults to "titles/full_access" 36 | """ 37 | authenticated_claims = event["requestContext"]["authorizer"]["claims"] 38 | 39 | try: 40 | request_body = json.loads(event.get("body") or "{}") 41 | except (TypeError, json.JSONDecodeError): 42 | logger.exception("Bad Request: No JSON content found") 43 | return response("Bad Request: No JSON content found", 400) 44 | 45 | try: 46 | validate(request_body, schema_definition) 47 | except ValidationError as error: 48 | validation_error = ( 49 | f"Validation Error {str(error.message)} " 50 | f"for item: {'/'.join([str(i) for i in error.path])}" 51 | ) 52 | logger.error(validation_error) 53 | return response(validation_error, 400) 54 | 55 | new_api_token = create_api_token( 56 | contributor_id=authenticated_claims["sub"], 57 | expires_in=get_expires_in(request_body.get("expires_in_days")), 58 | scope=get_scope_string(request_body.get("titles_in_scope")), 59 | ) 60 | 61 | try: 62 | write_token_to_table(authenticated_claims["sub"], new_api_token) 63 | except ClientError as error: 64 | logger.exception("Unable to write token entry.") 65 | raise 66 | 67 | return response( 68 | {"id": new_api_token["id"], "api_token": new_api_token["api_token"]}, 201 69 | ) 70 | 71 | 72 | def get_expires_in(time_in_days): 73 | if not time_in_days: 74 | time_in_days = 365 75 | return 60 * 60 * 24 * time_in_days 76 | 77 | 78 | def get_scope_string(title_ids): 79 | if not title_ids: 80 | return "titles-api/full_access" 81 | return " ".join([f"titles-api/{i}" for i in title_ids]) 82 | 83 | 84 | def create_api_token(contributor_id, expires_in, scope): 85 | token_id = str(uuid.uuid4()) 86 | token_secret = secrets.token_hex() 87 | 88 | issued_time = int(time.time()) 89 | expiration_time = issued_time + expires_in 90 | 91 | api_token = jwt.encode( 92 | { 93 | "sub": contributor_id, 94 | "token_use": "access", 95 | "scope": scope, 96 | "iss": f"https://contributors.{DOMAIN_NAME}", 97 | "aud": f"https://api.{DOMAIN_NAME}", 98 | "jti": token_id, 99 | "exp": expiration_time, 100 | "iat": issued_time, 101 | }, 102 | token_secret, 103 | algorithm="HS256", 104 | ).decode() 105 | 106 | return { 107 | "id": token_id, 108 | "secret": token_secret, 109 | "api_token": api_token, 110 | "expiration": expiration_time, 111 | } 112 | 113 | 114 | def write_token_to_table(contributor_id, token): 115 | communitypatchtable.put_item( 116 | Item={ 117 | "contributor_id": contributor_id, 118 | "type": f"TOKEN#{token['id']}", 119 | "token_id": token["id"], 120 | "token_secret": token["secret"], 121 | "ttl": token["expiration"], 122 | }, 123 | ConditionExpression="attribute_not_exists(#type)", 124 | ExpressionAttributeNames={"#type": "type"}, 125 | ) 126 | 127 | 128 | def response(message, status_code): 129 | if isinstance(message, str): 130 | message = {"message": message} 131 | 132 | return { 133 | "isBase64Encoded": False, 134 | "statusCode": status_code, 135 | "body": json.dumps(message), 136 | "headers": {"Content-Type": "application/json"}, 137 | } 138 | -------------------------------------------------------------------------------- /apis/contributors/src/create_api_token/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography 2 | jsonschema 3 | pyjwt 4 | -------------------------------------------------------------------------------- /apis/contributors/src/create_api_token/schemas/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "http://example.com/example.json", 4 | "type": "object", 5 | "properties": { 6 | "expires_in_days": { 7 | "$id": "#/properties/expires_in_days", 8 | "type": "integer", 9 | "minimum": 1, 10 | "maximum": 365, 11 | "default": 365, 12 | "examples": [ 13 | 365 14 | ] 15 | }, 16 | "titles_in_scope": { 17 | "$id": "#/properties/titles_in_scope", 18 | "type": "array", 19 | "default": [], 20 | "items": { 21 | "$id": "#/properties/titles_in_scope/items", 22 | "type": "string", 23 | "minLength": 1, 24 | "examples": [ 25 | "<<Title ID>>" 26 | ] 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /apis/contributors/src/get_contributors/index.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from operator import itemgetter 3 | import os 4 | 5 | from aws_xray_sdk.core import patch 6 | import boto3 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | patch(["boto3"]) 12 | 13 | CONTRIBUTORS_TABLE = os.getenv("CONTRIBUTORS_TABLE") 14 | DOMAIN_NAME = os.getenv("DOMAIN_NAME") 15 | TITLES_TABLE = os.getenv("TITLES_TABLE") 16 | 17 | dynamodb = boto3.resource("dynamodb") 18 | 19 | 20 | def lambda_handler(event, context): 21 | results = list() 22 | 23 | for contributor in scan_table(CONTRIBUTORS_TABLE): 24 | uri = "/".join(["jamf/v1", contributor["id"], "software"]) 25 | results.append( 26 | { 27 | "id": contributor["id"], 28 | "display_name": contributor["display_name"], 29 | "title_count": contributor["title_count"], 30 | "urn": uri, 31 | "url": f"https://{DOMAIN_NAME}/{uri}", 32 | } 33 | ) 34 | 35 | return sorted(results, key=itemgetter("title_count"), reverse=True), 200 36 | 37 | 38 | def scan_table(table_name): 39 | table = dynamodb.Table(table_name) 40 | 41 | results = table.scan() 42 | while True: 43 | for row in results["Items"]: 44 | yield row 45 | if results.get("LastEvaluatedKey"): 46 | results = dynamodb.scan(ExclusiveStartKey=results["LastEvaluatedKey"]) 47 | else: 48 | break 49 | -------------------------------------------------------------------------------- /apis/contributors/src/get_contributors/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/apis/contributors/src/get_contributors/requirements.txt -------------------------------------------------------------------------------- /apis/contributors/src/implicit_login/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | CLIENT_ID = os.getenv("CLIENT_ID") 4 | DOMAIN_NAME = os.getenv("DOMAIN_NAME") 5 | 6 | 7 | def lambda_handler(event, context): 8 | """Redirect for an Implicit auth flow.""" 9 | return { 10 | "isBase64Encoded": False, 11 | "statusCode": 301, 12 | "headers": { 13 | "Location": f"https://auth.{DOMAIN_NAME}/login?response_type=token&client_id={CLIENT_ID}&redirect_uri=https://{DOMAIN_NAME}" 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /apis/contributors/src/implicit_login/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/apis/contributors/src/implicit_login/requirements.txt -------------------------------------------------------------------------------- /apis/contributors/src/invalidate_api_token/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | 5 | import boto3 6 | from botocore.exceptions import ClientError 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | communitypatchtable = boto3.resource("dynamodb").Table( 12 | os.getenv("COMMUNITY_PATCH_TABLE") 13 | ) 14 | 15 | 16 | def lambda_handler(event, context): 17 | authenticated_claims = event["requestContext"]["authorizer"]["claims"] 18 | 19 | try: 20 | communitypatchtable.delete_item( 21 | Key={ 22 | "contributor_id": authenticated_claims["sub"], 23 | "type": f"TOKEN#{event['pathParameters']['token_id']}", 24 | }, 25 | ConditionExpression="attribute_exists(#type)", 26 | ExpressionAttributeNames={"#type": "type"}, 27 | ) 28 | except ClientError as error: 29 | if error.response["Error"]["Code"] == "ConditionalCheckFailedException": 30 | return response("Not Found", 404) 31 | else: 32 | raise 33 | 34 | return {"statusCode": 204} 35 | 36 | 37 | def response(message, status_code): 38 | if isinstance(message, str): 39 | message = {"message": message} 40 | 41 | return { 42 | "isBase64Encoded": False, 43 | "statusCode": status_code, 44 | "body": json.dumps(message), 45 | "headers": {"Content-Type": "application/json"}, 46 | } 47 | -------------------------------------------------------------------------------- /apis/contributors/src/invalidate_api_token/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/apis/contributors/src/invalidate_api_token/requirements.txt -------------------------------------------------------------------------------- /apis/contributors/src/oauth2_appleid_token/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | 5 | CLIENT_ID = os.getenv("CLIENT_ID") 6 | DOMAIN_NAME = os.getenv("DOMAIN_NAME") 7 | 8 | session = requests.Session() 9 | 10 | 11 | def lambda_handler(event, context): 12 | response = session.post( 13 | url=f"https://auth.{DOMAIN_NAME}/oauth2/token", 14 | headers={"Content-Type": "application/x-www-form-urlencoded"}, 15 | data={ 16 | "grant_type": "authorization_code", 17 | "client_id": CLIENT_ID, 18 | "redirect_uri": f"https://contributors.{DOMAIN_NAME}/oauth2/appleid/token", 19 | "code": event["queryStringParameters"]["code"], 20 | }, 21 | ) 22 | 23 | return { 24 | "isBase64Encoded": False, 25 | "statusCode": response.status_code, 26 | "body": response.text, 27 | "headers": {"Content-Type": "application/json"}, 28 | } 29 | -------------------------------------------------------------------------------- /apis/contributors/src/oauth2_appleid_token/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /apis/contributors/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | 4 | Parameters: 5 | 6 | Namespace: 7 | Type: String 8 | 9 | DomainName: 10 | Type: String 11 | 12 | HostedZoneId: 13 | Type: String 14 | 15 | RegionalCertificateArn: 16 | Type: AWS::SSM::Parameter::Value<String> 17 | 18 | CognitoUserPoolArn: 19 | Type: String 20 | 21 | AppleClientId: 22 | Type: String 23 | 24 | CommunityPatchTableName: 25 | Type: String 26 | 27 | # SAM Globals 28 | 29 | Globals: 30 | Function: 31 | Runtime: python3.7 32 | Handler: index.lambda_handler 33 | Tracing: PassThrough 34 | Environment: 35 | Variables: 36 | CLIENT_ID: !Ref AppleClientId 37 | COMMUNITY_PATCH_TABLE: !Ref CommunityPatchTableName 38 | DOMAIN_NAME: !Ref DomainName 39 | NAMESPACE: !Ref Namespace 40 | 41 | Resources: 42 | 43 | # API Gateway 44 | 45 | ApiGateway: 46 | Type: AWS::Serverless::Api 47 | Properties: 48 | StageName: Prod 49 | EndpointConfiguration: REGIONAL 50 | TracingEnabled: true 51 | Auth: 52 | Authorizers: 53 | CognitoAuthorizer: 54 | UserPoolArn: !Ref CognitoUserPoolArn 55 | 56 | ApiCustomDomain: 57 | Type: AWS::ApiGateway::DomainName 58 | Properties: 59 | DomainName: !Sub 'contributors.${DomainName}' 60 | RegionalCertificateArn: !Ref RegionalCertificateArn 61 | EndpointConfiguration: 62 | Types: 63 | - REGIONAL 64 | 65 | ApiBasePath: 66 | Type: AWS::ApiGateway::BasePathMapping 67 | Properties: 68 | DomainName: !Ref ApiCustomDomain 69 | RestApiId: !Ref ApiGateway 70 | Stage: Prod 71 | DependsOn: 72 | - ApiGateway 73 | - ApiGatewayProdStage 74 | - ApiCustomDomain 75 | 76 | RegionalRoute53Record: 77 | Type: AWS::Route53::RecordSet 78 | Properties: 79 | Name: !Sub 'contributors.${DomainName}' 80 | SetIdentifier: !Sub 'contributors-api-${AWS::Region}' 81 | AliasTarget: 82 | DNSName: !GetAtt ApiCustomDomain.RegionalDomainName 83 | HostedZoneId: !GetAtt ApiCustomDomain.RegionalHostedZoneId 84 | HostedZoneId: !Ref HostedZoneId 85 | Region: !Ref AWS::Region 86 | Type: A 87 | 88 | # Lambda 89 | 90 | AppleIdLogin: 91 | Type: AWS::Serverless::Function 92 | Properties: 93 | CodeUri: ./src/implicit_login 94 | Events: 95 | GetToken: 96 | Type: Api 97 | Properties: 98 | Path: /login 99 | Method: get 100 | RestApiId: 101 | Ref: ApiGateway 102 | 103 | Oauth2AppleIdToken: 104 | Type: AWS::Serverless::Function 105 | Properties: 106 | CodeUri: ./src/oauth2_appleid_token 107 | Events: 108 | GetToken: 109 | Type: Api 110 | Properties: 111 | Path: /oauth2/appleid/token 112 | Method: get 113 | RestApiId: 114 | Ref: ApiGateway 115 | 116 | ApiContributorsGet: 117 | Type: AWS::Serverless::Function 118 | Properties: 119 | CodeUri: ./src/get_contributors 120 | Policies: 121 | - DynamoDBReadPolicy: 122 | TableName: !Ref CommunityPatchTableName 123 | Events: 124 | ApiContributorRegistration: 125 | Type: Api 126 | Properties: 127 | Path: /v1/contributors 128 | Method: get 129 | RestApiId: 130 | Ref: ApiGateway 131 | 132 | CreateApiToken: 133 | Type: AWS::Serverless::Function 134 | Properties: 135 | CodeUri: ./src/create_api_token 136 | Policies: 137 | - DynamoDBCrudPolicy: 138 | TableName: !Ref CommunityPatchTableName 139 | Events: 140 | ApiContributorRegistration: 141 | Type: Api 142 | Properties: 143 | Path: /v1/tokens 144 | Method: post 145 | RestApiId: 146 | Ref: ApiGateway 147 | Auth: 148 | Authorizer: CognitoAuthorizer 149 | AuthorizationScopes: 150 | - contributors-api/full_access 151 | 152 | InvalidateApiToken: 153 | Type: AWS::Serverless::Function 154 | Properties: 155 | CodeUri: ./src/invalidate_api_token 156 | Policies: 157 | - DynamoDBCrudPolicy: 158 | TableName: !Ref CommunityPatchTableName 159 | Events: 160 | ApiContributorRegistration: 161 | Type: Api 162 | Properties: 163 | Path: /v1/tokens/{token_id}/invalidate 164 | Method: post 165 | RestApiId: 166 | Ref: ApiGateway 167 | Auth: 168 | Authorizer: CognitoAuthorizer 169 | AuthorizationScopes: 170 | - contributors-api/full_access 171 | -------------------------------------------------------------------------------- /apis/jamf/src/read_titles/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | 5 | import boto3 6 | from boto3.dynamodb.conditions import Key, Attr 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | communitypatchtable = boto3.resource("dynamodb").Table( 12 | os.getenv("COMMUNITY_PATCH_TABLE") 13 | ) 14 | 15 | 16 | def lambda_handler(event, context): 17 | # There's an issue with the HTTP API event where the "resource" key is not the route 18 | # string defined in the template with the parameters but is the same value as the 19 | # "path" key. The following attempts to work around this. 20 | 21 | contributor_id = event["pathParameters"]["contributor_id"] 22 | title_ids = event["pathParameters"].get("title_ids") # /software 23 | title_id = event["pathParameters"].get("title_id") # /patch 24 | 25 | if event["resource"] == f"/v1/{contributor_id}/software": 26 | result = communitypatchtable.query( 27 | IndexName="ContributorSummaries", 28 | KeyConditionExpression=Key("contributor_id").eq(contributor_id), 29 | ) 30 | 31 | return response([i["summary"] for i in result["Items"]], 200) 32 | 33 | elif event["resource"] == f"/v1/{contributor_id}/software/{title_ids}": 34 | results = list() 35 | 36 | for i in set(title_ids.split(",")): 37 | query_result = communitypatchtable.query( 38 | IndexName="ContributorSummaries", 39 | KeyConditionExpression=Key("contributor_id").eq(contributor_id) 40 | & Key("title_id").eq(i.lower()), 41 | ) 42 | if query_result.get("Items"): 43 | results.append(query_result["Items"][0]["summary"]) 44 | 45 | return response(results, 200) 46 | 47 | elif event["resource"] == f"/v1/{contributor_id}/patch/{title_id}": 48 | # Returns the full definition body of the selected title for a contributor 49 | 50 | result = communitypatchtable.get_item( 51 | Key={"contributor_id": contributor_id, "type": f"TITLE#{title_id.lower()}"} 52 | ) 53 | try: 54 | return { 55 | "statusCode": 200, 56 | "body": result["Item"]["body"], 57 | "headers": {"Content-Type": "application/json"}, 58 | } 59 | except KeyError: 60 | return response("Not Found", 404) 61 | 62 | else: 63 | return response("Not Found", 404) 64 | 65 | 66 | def response(message, status_code): 67 | if isinstance(message, str): 68 | message = {"message": message} 69 | 70 | return { 71 | "isBase64Encoded": False, 72 | "statusCode": status_code, 73 | "body": json.dumps(message), 74 | "headers": {"Content-Type": "application/json"}, 75 | } 76 | -------------------------------------------------------------------------------- /apis/jamf/src/read_titles/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/apis/jamf/src/read_titles/requirements.txt -------------------------------------------------------------------------------- /apis/jamf/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Jamf Pro endpoints for Community Patch 4 | 5 | Parameters: 6 | 7 | DomainName: 8 | Type: String 9 | 10 | HostedZoneId: 11 | Type: String 12 | 13 | RegionalCertificateArn: 14 | Type: AWS::SSM::Parameter::Value<String> 15 | 16 | CommunityPatchTableName: 17 | Type: String 18 | 19 | # SAM Globals 20 | 21 | Globals: 22 | Function: 23 | Runtime: python3.7 24 | Handler: index.lambda_handler 25 | Tracing: PassThrough 26 | Environment: 27 | Variables: 28 | COMMUNITY_PATCH_TABLE: !Ref CommunityPatchTableName 29 | 30 | Resources: 31 | 32 | # HTTP API 33 | 34 | Api: 35 | Type: AWS::Serverless::HttpApi 36 | 37 | ApiCustomDomain: 38 | Type: AWS::ApiGatewayV2::DomainName 39 | Properties: 40 | DomainName: !Sub 'jamf.${DomainName}' 41 | DomainNameConfigurations: 42 | - CertificateArn: !Ref RegionalCertificateArn 43 | EndpointType: REGIONAL 44 | 45 | ApiMapping: 46 | Type: AWS::ApiGatewayV2::ApiMapping 47 | Properties: 48 | DomainName: !Ref ApiCustomDomain 49 | ApiId: !Ref Api 50 | Stage: "$default" 51 | 52 | RegionalRoute53Record: 53 | Type: AWS::Route53::RecordSet 54 | Properties: 55 | Name: !Sub 'jamf.${DomainName}' 56 | SetIdentifier: !Sub 'jamf-api-${AWS::Region}' 57 | AliasTarget: 58 | DNSName: !GetAtt ApiCustomDomain.RegionalDomainName 59 | HostedZoneId: !GetAtt ApiCustomDomain.RegionalHostedZoneId 60 | HostedZoneId: !Ref HostedZoneId 61 | Region: !Ref AWS::Region 62 | Type: A 63 | 64 | # Lambda 65 | 66 | ReadTitles: 67 | Type: AWS::Serverless::Function 68 | Properties: 69 | CodeUri: ./src/read_titles 70 | Policies: 71 | - DynamoDBReadPolicy: 72 | TableName: !Ref CommunityPatchTableName 73 | Events: 74 | GetAllSoftware: 75 | Type: HttpApi 76 | Properties: 77 | Path: /v1/{contributor_id}/software 78 | Method: get 79 | ApiId: !Ref Api 80 | GetSelectSoftware: 81 | Type: HttpApi 82 | Properties: 83 | Path: /v1/{contributor_id}/software/{title_ids} 84 | Method: get 85 | ApiId: !Ref Api 86 | GetPatch: 87 | Type: HttpApi 88 | Properties: 89 | Path: /v1/{contributor_id}/patch/{title_id} 90 | Method: get 91 | ApiId: !Ref Api 92 | -------------------------------------------------------------------------------- /apis/titles/TitlesAPI.md: -------------------------------------------------------------------------------- 1 | # Titles API 2 | 3 | WIP 4 | -------------------------------------------------------------------------------- /apis/titles/src/authorizer/index.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import boto3 5 | import jwt 6 | 7 | logger = logging.getLogger() 8 | logger.setLevel(logging.INFO) 9 | 10 | DOMAIN_NAME = os.getenv("DOMAIN_NAME") 11 | 12 | communitypatchtable = boto3.resource("dynamodb").Table( 13 | os.getenv("COMMUNITY_PATCH_TABLE") 14 | ) 15 | 16 | 17 | def lambda_handler(event, context): 18 | """Details on errors must never be provided back to the authenticating client. 19 | 20 | Exceptions are logged, but ANY exception raised during token validation must result 21 | in a generic Unauthorized response. 22 | """ 23 | token = event["authorizationToken"] 24 | 25 | try: 26 | unverified_claims = jwt.decode(token, verify=False) 27 | token_entry = token_lookup(unverified_claims["sub"], unverified_claims["jti"]) 28 | jwt.decode( 29 | token, 30 | token_entry["token_secret"], 31 | issuer=f"https://contributors.{DOMAIN_NAME}", 32 | audience=f"https://api.{DOMAIN_NAME}", 33 | algorithms=["HS256"], 34 | ) 35 | except: 36 | logger.exception("Token verification failed") 37 | raise Exception("Unauthorized") 38 | 39 | return generate_policy(token, "Allow", event["methodArn"], unverified_claims) 40 | 41 | 42 | def token_lookup(contributor_id, token_id): 43 | response = communitypatchtable.get_item( 44 | Key={"contributor_id": contributor_id, "type": f"TOKEN#{token_id}"} 45 | ) 46 | return response["Item"] 47 | 48 | 49 | def generate_policy(principal_id, effect=None, resource=None, context=None): 50 | auth_response = {"principalId": principal_id} 51 | 52 | if effect and resource: 53 | auth_response["policyDocument"] = { 54 | "Version": "2012-10-17", 55 | "Statement": [ 56 | { 57 | "Action": "execute-api:Invoke", 58 | "Effect": effect, 59 | "Resource": "/".join(resource.split("/")[:2] + ["*"]), 60 | } 61 | ], 62 | } 63 | 64 | if context: 65 | # Context path in API event becomes "event['requestContext']['authorizer']" 66 | auth_response["context"] = dict() 67 | for k, v in context.items(): 68 | auth_response["context"][k] = str(v) 69 | 70 | logger.info(auth_response) 71 | return auth_response 72 | -------------------------------------------------------------------------------- /apis/titles/src/authorizer/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography 2 | pyjwt 3 | -------------------------------------------------------------------------------- /apis/titles/src/create_title/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | 5 | import boto3 6 | from botocore.exceptions import ClientError 7 | from jsonschema import validate, ValidationError 8 | 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | 13 | def read_schema(schema): 14 | with open(f"schemas/{schema}.json", "r") as f_obj: 15 | return json.load(f_obj) 16 | 17 | 18 | communitypatchtable = boto3.resource("dynamodb").Table( 19 | os.getenv("COMMUNITY_PATCH_TABLE") 20 | ) 21 | 22 | schema_definition = read_schema("full_definition") 23 | 24 | 25 | def lambda_handler(event, context): 26 | # Not consistent with Cognito auth 27 | authenticated_claims = event["requestContext"]["authorizer"] 28 | 29 | try: 30 | title_body = json.loads(event["body"]) 31 | except (TypeError, json.JSONDecodeError): 32 | logger.exception("Bad Request: No JSON content found") 33 | return response("Bad Request: No JSON content found", 400) 34 | 35 | try: 36 | validate(title_body, schema_definition) 37 | except ValidationError as error: 38 | validation_error = ( 39 | f"Validation Error {str(error.message)} " 40 | f"for item: {'/'.join([str(i) for i in error.path])}" 41 | ) 42 | logger.error(validation_error) 43 | return response(validation_error, 400) 44 | 45 | try: 46 | create_table_entry(authenticated_claims["sub"], title_body) 47 | except ClientError as error: 48 | if error.response["Error"]["Code"] == "ConditionalCheckFailedException": 49 | return response( 50 | f"Conflict: You have already created a title with the ID '{title_body['id']}'", 51 | 409, 52 | ) 53 | else: 54 | logger.exception("Unknown ClientError writing new title.") 55 | return response(f"Internal Server Error", 500) 56 | 57 | return response(f"Title '{title_body['id']}' created", 201) 58 | 59 | 60 | def create_table_entry(contributor_id, title_body): 61 | communitypatchtable.put_item( 62 | Item={ 63 | "contributor_id": contributor_id, 64 | "type": f"TITLE#{title_body['id'].lower()}", 65 | "search_index": "TITLE", 66 | "aws_region": os.getenv("AWS_REGION"), 67 | "title_id": title_body["id"].lower(), 68 | "body": json.dumps(title_body), 69 | "summary": { 70 | "id": title_body["id"], 71 | "name": title_body["name"], 72 | "publisher": title_body["publisher"], 73 | "currentVersion": title_body["currentVersion"], 74 | "lastModified": title_body["lastModified"], 75 | }, 76 | }, 77 | ConditionExpression="attribute_not_exists(#type)", 78 | ExpressionAttributeNames={"#type": "type"}, 79 | ) 80 | 81 | 82 | def response(message, status_code): 83 | if isinstance(message, str): 84 | message = {"message": message} 85 | 86 | return { 87 | "isBase64Encoded": False, 88 | "statusCode": status_code, 89 | "body": json.dumps(message), 90 | "headers": {"Content-Type": "application/json"}, 91 | } 92 | -------------------------------------------------------------------------------- /apis/titles/src/create_title/requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema -------------------------------------------------------------------------------- /apis/titles/src/create_title/schemas/full_definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "http://example.com/example.json", 3 | "type": "object", 4 | "definitions": {}, 5 | "$schema": "http://json-schema.org/draft-06/schema#", 6 | "properties": { 7 | "name": { 8 | "$id": "/properties/name", 9 | "type": "string", 10 | "examples": [ 11 | "Composer" 12 | ] 13 | }, 14 | "publisher": { 15 | "$id": "/properties/publisher", 16 | "type": "string", 17 | "examples": [ 18 | "Jamf" 19 | ] 20 | }, 21 | "appName": { 22 | "$id": "/properties/appName", 23 | "type": ["string", "null"], 24 | "examples": [ 25 | "Composer.app" 26 | ] 27 | }, 28 | "bundleId": { 29 | "$id": "/properties/bundleId", 30 | "type": ["string", "null"], 31 | "examples": [ 32 | "com.jamfsoftware.Composer" 33 | ] 34 | }, 35 | "lastModified": { 36 | "$id": "/properties/lastModified", 37 | "type": "string", 38 | "examples": [ 39 | "2017-12-20T16:11:01Z" 40 | ] 41 | }, 42 | "currentVersion": { 43 | "$id": "/properties/currentVersion", 44 | "type": "string", 45 | "examples": [ 46 | "10.1.1" 47 | ] 48 | }, 49 | "requirements": { 50 | "$id": "/properties/requirements", 51 | "type": "array", 52 | "items": { 53 | "$id": "/properties/requirements/items", 54 | "type": "object", 55 | "properties": { 56 | "name": { 57 | "$id": "/properties/requirements/items/properties/name", 58 | "type": "string", 59 | "examples": [ 60 | "Application Bundle ID" 61 | ] 62 | }, 63 | "operator": { 64 | "$id": "/properties/requirements/items/properties/operator", 65 | "type": "string", 66 | "examples": [ 67 | "is" 68 | ] 69 | }, 70 | "value": { 71 | "$id": "/properties/requirements/items/properties/value", 72 | "type": "string", 73 | "examples": [ 74 | "com.jamfsoftware.Composer" 75 | ] 76 | }, 77 | "type": { 78 | "$id": "/properties/requirements/items/properties/type", 79 | "type": "string", 80 | "examples": [ 81 | "recon" 82 | ] 83 | }, 84 | "and": { 85 | "$id": "/properties/requirements/items/properties/and", 86 | "type": "boolean", 87 | "examples": [ 88 | true 89 | ] 90 | } 91 | }, 92 | "required": [ 93 | "name", 94 | "operator", 95 | "value", 96 | "type", 97 | "and" 98 | ] 99 | } 100 | }, 101 | "patches": { 102 | "$id": "/properties/patches", 103 | "type": "array", 104 | "items": { 105 | "$id": "/properties/patches/items", 106 | "type": "object", 107 | "properties": { 108 | "version": { 109 | "$id": "/properties/patches/items/properties/version", 110 | "type": "string", 111 | "examples": [ 112 | "10.1.1" 113 | ] 114 | }, 115 | "releaseDate": { 116 | "$id": "/properties/patches/items/properties/releaseDate", 117 | "type": "string", 118 | "examples": [ 119 | "2017-12-20T10:08:38.270Z" 120 | ] 121 | }, 122 | "standalone": { 123 | "$id": "/properties/patches/items/properties/standalone", 124 | "type": "boolean", 125 | "examples": [ 126 | true 127 | ] 128 | }, 129 | "minimumOperatingSystem": { 130 | "$id": "/properties/patches/items/properties/minimumOperatingSystem", 131 | "type": "string", 132 | "examples": [ 133 | "10.9" 134 | ] 135 | }, 136 | "reboot": { 137 | "$id": "/properties/patches/items/properties/reboot", 138 | "type": "boolean", 139 | "examples": [ 140 | false 141 | ] 142 | }, 143 | "killApps": { 144 | "$id": "/properties/patches/items/properties/killApps", 145 | "type": "array", 146 | "items": { 147 | "$id": "/properties/patches/items/properties/killApps/items", 148 | "type": "object", 149 | "properties": { 150 | "bundleId": { 151 | "$id": "/properties/patches/items/properties/killApps/items/properties/bundleId", 152 | "type": "string", 153 | "examples": [ 154 | "com.jamfsoftware.Composer" 155 | ] 156 | }, 157 | "appName": { 158 | "$id": "/properties/patches/items/properties/killApps/items/properties/appName", 159 | "type": "string", 160 | "examples": [ 161 | "Composer.app" 162 | ] 163 | } 164 | }, 165 | "required": [ 166 | "bundleId", 167 | "appName" 168 | ] 169 | } 170 | }, 171 | "components": { 172 | "$id": "/properties/patches/items/properties/components", 173 | "type": "array", 174 | "items": { 175 | "$id": "/properties/patches/items/properties/components/items", 176 | "type": "object", 177 | "properties": { 178 | "name": { 179 | "$id": "/properties/patches/items/properties/components/items/properties/name", 180 | "type": "string", 181 | "examples": [ 182 | "Composer" 183 | ] 184 | }, 185 | "version": { 186 | "$id": "/properties/patches/items/properties/components/items/properties/version", 187 | "type": "string", 188 | "examples": [ 189 | "10.1.1" 190 | ] 191 | }, 192 | "criteria": { 193 | "$id": "/properties/patches/items/properties/components/items/properties/criteria", 194 | "type": "array", 195 | "items": { 196 | "$id": "/properties/patches/items/properties/components/items/properties/criteria/items", 197 | "type": "object", 198 | "properties": { 199 | "name": { 200 | "$id": "/properties/patches/items/properties/components/items/properties/criteria/items/properties/name", 201 | "type": "string", 202 | "examples": [ 203 | "Application Bundle ID" 204 | ] 205 | }, 206 | "operator": { 207 | "$id": "/properties/patches/items/properties/components/items/properties/criteria/items/properties/operator", 208 | "type": "string", 209 | "examples": [ 210 | "is" 211 | ] 212 | }, 213 | "value": { 214 | "$id": "/properties/patches/items/properties/components/items/properties/criteria/items/properties/value", 215 | "type": "string", 216 | "examples": [ 217 | "com.jamfsoftware.Composer" 218 | ] 219 | }, 220 | "type": { 221 | "$id": "/properties/patches/items/properties/components/items/properties/criteria/items/properties/type", 222 | "type": "string", 223 | "examples": [ 224 | "recon" 225 | ] 226 | }, 227 | "and": { 228 | "$id": "/properties/patches/items/properties/components/items/properties/criteria/items/properties/and", 229 | "type": "boolean", 230 | "examples": [ 231 | true 232 | ] 233 | } 234 | }, 235 | "required": [ 236 | "name", 237 | "operator", 238 | "value", 239 | "type" 240 | ] 241 | } 242 | } 243 | }, 244 | "required": [ 245 | "name", 246 | "version", 247 | "criteria" 248 | ] 249 | } 250 | }, 251 | "capabilities": { 252 | "$id": "/properties/patches/items/properties/capabilities", 253 | "type": "array", 254 | "items": { 255 | "$id": "/properties/patches/items/properties/capabilities/items", 256 | "type": "object", 257 | "properties": { 258 | "name": { 259 | "$id": "/properties/patches/items/properties/capabilities/items/properties/name", 260 | "type": "string", 261 | "examples": [ 262 | "Operating System Version" 263 | ] 264 | }, 265 | "operator": { 266 | "$id": "/properties/patches/items/properties/capabilities/items/properties/operator", 267 | "type": "string", 268 | "examples": [ 269 | "greater than or equal" 270 | ] 271 | }, 272 | "value": { 273 | "$id": "/properties/patches/items/properties/capabilities/items/properties/value", 274 | "type": "string", 275 | "examples": [ 276 | "10.9" 277 | ] 278 | }, 279 | "type": { 280 | "$id": "/properties/patches/items/properties/capabilities/items/properties/type", 281 | "type": "string", 282 | "examples": [ 283 | "recon" 284 | ] 285 | } 286 | }, 287 | "required": [ 288 | "name", 289 | "operator", 290 | "value", 291 | "type" 292 | ] 293 | } 294 | }, 295 | "dependencies": { 296 | "$id": "/properties/patches/items/properties/dependencies", 297 | "type": "array" 298 | } 299 | }, 300 | "required": [ 301 | "version", 302 | "releaseDate", 303 | "standalone", 304 | "minimumOperatingSystem", 305 | "reboot", 306 | "killApps", 307 | "components", 308 | "capabilities" 309 | ] 310 | } 311 | }, 312 | "extensionAttributes": { 313 | "$id": "/properties/extensionAttributes", 314 | "type": "array", 315 | "items": { 316 | "$id": "/properties/extensionAttributes/items", 317 | "type": "object", 318 | "properties": { 319 | "key": { 320 | "$id": "/properties/extensionAttributes/items/properties/key", 321 | "type": "string", 322 | "examples": [ 323 | "composer-ea" 324 | ] 325 | }, 326 | "value": { 327 | "$id": "/properties/extensionAttributes/items/properties/value", 328 | "type": "string", 329 | "examples": [ 330 | "<Base 64 encoded string>" 331 | ] 332 | }, 333 | "displayName": { 334 | "$id": "/properties/extensionAttributes/items/properties/displayName", 335 | "type": "string", 336 | "examples": [ 337 | "Composer" 338 | ] 339 | } 340 | }, 341 | "required": [ 342 | "key", 343 | "value", 344 | "displayName" 345 | ] 346 | } 347 | }, 348 | "id": { 349 | "$id": "/properties/id", 350 | "type": "string", 351 | "examples": [ 352 | "Composer" 353 | ] 354 | } 355 | }, 356 | "required": [ 357 | "name", 358 | "publisher", 359 | "appName", 360 | "bundleId", 361 | "lastModified", 362 | "currentVersion", 363 | "requirements", 364 | "patches", 365 | "extensionAttributes", 366 | "id" 367 | ] 368 | } 369 | -------------------------------------------------------------------------------- /apis/titles/src/delete_title/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | 5 | import boto3 6 | from botocore.exceptions import ClientError 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | communitypatchtable = boto3.resource("dynamodb").Table( 12 | os.getenv("COMMUNITY_PATCH_TABLE") 13 | ) 14 | 15 | 16 | def lambda_handler(event, context): 17 | authenticated_claims = event["requestContext"]["authorizer"] 18 | title_id = event["pathParameters"]["title_id"] 19 | 20 | try: 21 | communitypatchtable.delete_item( 22 | Key={ 23 | "contributor_id": authenticated_claims["sub"], 24 | "type": f"TITLE#{title_id.lower()}", 25 | }, 26 | ConditionExpression="attribute_exists(#type)", 27 | ExpressionAttributeNames={"#type": "type"}, 28 | ) 29 | except ClientError as error: 30 | if error.response["Error"]["Code"] == "ConditionalCheckFailedException": 31 | return response("Not Found", 404) 32 | else: 33 | raise 34 | 35 | return response(f"Title '{title_id}' deleted", 200) 36 | 37 | 38 | def response(message, status_code): 39 | if isinstance(message, str): 40 | message = {"message": message} 41 | 42 | return { 43 | "isBase64Encoded": False, 44 | "statusCode": status_code, 45 | "body": json.dumps(message), 46 | "headers": {"Content-Type": "application/json"}, 47 | } 48 | -------------------------------------------------------------------------------- /apis/titles/src/delete_title/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/apis/titles/src/delete_title/requirements.txt -------------------------------------------------------------------------------- /apis/titles/src/read_titles/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | 5 | import boto3 6 | from boto3.dynamodb.conditions import Key 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | communitypatchtable = boto3.resource("dynamodb").Table( 12 | os.getenv("COMMUNITY_PATCH_TABLE") 13 | ) 14 | 15 | 16 | def lambda_handler(event, context): 17 | # Not consistent with Cognito auth 18 | authenticated_claims = event["requestContext"]["authorizer"] 19 | 20 | if event["resource"] == "/v1/titles": 21 | result = communitypatchtable.query( 22 | IndexName="ContributorSummaries", 23 | KeyConditionExpression=Key("contributor_id").eq( 24 | authenticated_claims["sub"] 25 | ), 26 | ) 27 | return response({"titles": [i["summary"] for i in result["Items"]]}, 200) 28 | 29 | elif event["resource"] == "/v1/titles/{title_id}": 30 | title_id = event["pathParameters"]["title_id"] 31 | 32 | result = communitypatchtable.get_item( 33 | Key={ 34 | "contributor_id": authenticated_claims["sub"], 35 | "type": f"TITLE#{title_id.lower()}", 36 | } 37 | ) 38 | try: 39 | return { 40 | "statusCode": 200, 41 | "body": result["Item"]["body"], 42 | "headers": {"Content-Type": "application/json"}, 43 | } 44 | except KeyError: 45 | return response("Not Found", 404) 46 | 47 | 48 | def response(message, status_code): 49 | if isinstance(message, str): 50 | message = {"message": message} 51 | 52 | return { 53 | "isBase64Encoded": False, 54 | "statusCode": status_code, 55 | "body": json.dumps(message), 56 | "headers": {"Content-Type": "application/json"}, 57 | } 58 | -------------------------------------------------------------------------------- /apis/titles/src/read_titles/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/apis/titles/src/read_titles/requirements.txt -------------------------------------------------------------------------------- /apis/titles/src/update_title_version/index.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import json 3 | import logging 4 | import os 5 | 6 | import boto3 7 | from botocore.exceptions import ClientError 8 | from jsonschema import validate, ValidationError 9 | 10 | logger = logging.getLogger() 11 | logger.setLevel(logging.INFO) 12 | 13 | 14 | def read_schema(schema): 15 | with open(f"schemas/{schema}.json", "r") as f_obj: 16 | return json.load(f_obj) 17 | 18 | 19 | communitypatchtable = boto3.resource("dynamodb").Table( 20 | os.getenv("COMMUNITY_PATCH_TABLE") 21 | ) 22 | 23 | schema_definition = read_schema("version") 24 | 25 | 26 | class ApiException(Exception): 27 | status_code = 500 28 | 29 | 30 | class BadRequest(ApiException): 31 | status_code = 400 32 | 33 | 34 | class NotFound(ApiException): 35 | status_code = 404 36 | 37 | 38 | class Conflict(ApiException): 39 | status_code = 409 40 | 41 | 42 | def lambda_handler(event, context): 43 | # Not consistent with Cognito auth 44 | authenticated_claims = event["requestContext"]["authorizer"] 45 | 46 | try: 47 | current_title_body = read_title( 48 | authenticated_claims["sub"], event["pathParameters"]["title_id"] 49 | ) 50 | except KeyError: 51 | return response("Not Found", 404) 52 | 53 | # ADD VERSION 54 | if ( 55 | event["resource"] == "/v1/titles/{title_id}/versions" 56 | and event["httpMethod"] == "POST" 57 | ): 58 | try: 59 | version_body = json.loads(event["body"]) 60 | except (TypeError, json.JSONDecodeError): 61 | logger.exception("Bad Request: No JSON content found") 62 | return response("Bad Request: No JSON content found", 400) 63 | 64 | try: 65 | validate(version_body, schema_definition) 66 | except ValidationError as error: 67 | validation_error = ( 68 | f"Validation Error {str(error.message)} " 69 | f"for item: {'/'.join([str(i) for i in error.path])}" 70 | ) 71 | logger.error(validation_error) 72 | return response(validation_error, 400) 73 | 74 | try: 75 | updated_title_body = add_version( 76 | current_title_body, version_body, event["queryStringParameters"] 77 | ) 78 | update_title(authenticated_claims["sub"], updated_title_body) 79 | return response( 80 | f"Version '{version_body['version']}' added to title '{updated_title_body['id']}'", 81 | 201, 82 | ) 83 | except ApiException as error: 84 | return response(str(error), error.status_code) 85 | 86 | # DELETE VERSION 87 | elif ( 88 | event["resource"] == "/v1/titles/{title_id}/versions/{version}" 89 | and event["httpMethod"] == "DELETE" 90 | ): 91 | target_version = event["pathParameters"]["version"] 92 | try: 93 | updated_title_body = delete_version(current_title_body, target_version) 94 | update_title(authenticated_claims["sub"], updated_title_body) 95 | return response(f"Version '{target_version}' deleted from title", 200,) 96 | except ApiException as error: 97 | return response(str(error), error.status_code) 98 | 99 | else: 100 | return response("Not Found", 404) 101 | 102 | 103 | def read_title(contributor_id, title_id): 104 | result = communitypatchtable.get_item( 105 | Key={"contributor_id": contributor_id, "type": f"TITLE#{title_id.lower()}"} 106 | ) 107 | return json.loads(result["Item"]["body"]) 108 | 109 | 110 | def add_version(title_body, version_body, query_string_parameters): 111 | if version_body["version"] in [ 112 | patch_["version"] for patch_ in title_body["patches"] 113 | ]: 114 | logger.error(f"Conflicting version supplied: '{version_body['version']}'") 115 | raise Conflict(f"Conflict: The version '{version_body['version']}' exists") 116 | 117 | try: 118 | target_index = get_index(query_string_parameters, title_body["patches"]) 119 | except ValueError as error: 120 | raise BadRequest(f"Bad Request: {str(error)}") 121 | 122 | logger.info(f"Updating the definition with new version: {version_body['version']}") 123 | title_body["patches"].insert(target_index, version_body) 124 | 125 | # Use the version of the first patch after the insert operation above 126 | title_body["currentVersion"] = title_body["patches"][0]["version"] 127 | title_body["lastModified"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") 128 | 129 | return title_body 130 | 131 | 132 | def delete_version(title_body, target_version): 133 | index = next( 134 | ( 135 | idx 136 | for (idx, d) in enumerate(title_body["patches"]) 137 | if d["version"] == target_version 138 | ), 139 | None, 140 | ) 141 | 142 | if index is None: 143 | raise BadRequest("Not Found") 144 | 145 | if len(title_body["patches"]) < 2: 146 | raise BadRequest("A title must contain at least 1 version") 147 | 148 | logger.info(f"Removing version from the definition: {target_version}") 149 | title_body["patches"].pop(index) 150 | 151 | # Use the version of the first patch after the delete operation above 152 | title_body["currentVersion"] = title_body["patches"][0]["version"] 153 | title_body["lastModified"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") 154 | 155 | return title_body 156 | 157 | 158 | def get_index(qs_params, title_patches): 159 | """If 'insert_after' or 'insert_before' were passed as parameters, return 160 | the target index for the provided target version. 161 | 162 | If 'params' is 'None' or empty, return 0. 163 | 164 | :param qs_params: Query string parameters 165 | :type qs_params: dict or None 166 | 167 | :param list title_patches: The 'patches' array from a definition 168 | """ 169 | if not qs_params: 170 | return 0 171 | 172 | if all(i in qs_params.keys() for i in ["insert_after", "insert_before"]): 173 | raise ValueError("Conflicting parameters provided") 174 | 175 | index = None 176 | if any(i in qs_params.keys() for i in ["insert_after", "insert_before"]): 177 | 178 | if qs_params.get("insert_after"): 179 | index = ( 180 | next( 181 | ( 182 | idx 183 | for (idx, d) in enumerate(title_patches) 184 | if d["version"] == qs_params.get("insert_after") 185 | ), 186 | None, 187 | ) 188 | + 1 189 | ) 190 | 191 | elif qs_params.get("insert_before"): 192 | index = next( 193 | ( 194 | idx 195 | for (idx, d) in enumerate(title_patches) 196 | if d["version"] == qs_params.get("insert_before") 197 | ), 198 | None, 199 | ) 200 | 201 | else: 202 | raise ValueError("Parameter has no value") 203 | 204 | if index is None: 205 | raise ValueError("Provided version not found") 206 | 207 | return index 208 | 209 | 210 | def update_title(contributor_id, title_body): 211 | communitypatchtable.update_item( 212 | Key={ 213 | "contributor_id": contributor_id, 214 | "type": f"TITLE#{title_body['id'].lower()}", 215 | }, 216 | UpdateExpression="set body = :bd, " 217 | "summary.currentVersion = :cv, " 218 | "summary.lastModified = :lm", 219 | ExpressionAttributeValues={ 220 | ":bd": json.dumps(title_body), 221 | ":cv": title_body["currentVersion"], 222 | ":lm": title_body["lastModified"], 223 | }, 224 | ReturnValues="UPDATED_NEW", 225 | ) 226 | 227 | 228 | def response(message, status_code): 229 | if isinstance(message, str): 230 | message = {"message": message} 231 | 232 | return { 233 | "isBase64Encoded": False, 234 | "statusCode": status_code, 235 | "body": json.dumps(message), 236 | "headers": {"Content-Type": "application/json"}, 237 | } 238 | -------------------------------------------------------------------------------- /apis/titles/src/update_title_version/requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema 2 | 3 | -------------------------------------------------------------------------------- /apis/titles/src/update_title_version/schemas/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "http://example.com/example.json", 3 | "type": "object", 4 | "definitions": {}, 5 | "$schema": "http://json-schema.org/draft-06/schema#", 6 | "properties": { 7 | "version": { 8 | "$id": "/properties/version", 9 | "type": "string", 10 | "examples": [ 11 | "10.1.1" 12 | ] 13 | }, 14 | "releaseDate": { 15 | "$id": "/properties/releaseDate", 16 | "type": "string", 17 | "examples": [ 18 | "2017-12-20T10:08:38.270Z" 19 | ] 20 | }, 21 | "standalone": { 22 | "$id": "/properties/standalone", 23 | "type": "boolean", 24 | "examples": [ 25 | true 26 | ] 27 | }, 28 | "minimumOperatingSystem": { 29 | "$id": "/properties/minimumOperatingSystem", 30 | "type": "string", 31 | "examples": [ 32 | "10.9" 33 | ] 34 | }, 35 | "reboot": { 36 | "$id": "/properties/reboot", 37 | "type": "boolean", 38 | "examples": [ 39 | false 40 | ] 41 | }, 42 | "killApps": { 43 | "$id": "/properties/killApps", 44 | "type": "array", 45 | "items": { 46 | "$id": "/properties/killApps/items", 47 | "type": "object", 48 | "properties": { 49 | "bundleId": { 50 | "$id": "/properties/killApps/items/properties/bundleId", 51 | "type": "string", 52 | "examples": [ 53 | "com.jamfsoftware.Composer" 54 | ] 55 | }, 56 | "appName": { 57 | "$id": "/properties/killApps/items/properties/appName", 58 | "type": "string", 59 | "examples": [ 60 | "Composer.app" 61 | ] 62 | } 63 | }, 64 | "required": [ 65 | "bundleId", 66 | "appName" 67 | ] 68 | } 69 | }, 70 | "components": { 71 | "$id": "/properties/components", 72 | "type": "array", 73 | "items": { 74 | "$id": "/properties/components/items", 75 | "type": "object", 76 | "properties": { 77 | "name": { 78 | "$id": "/properties/components/items/properties/name", 79 | "type": "string", 80 | "examples": [ 81 | "Composer" 82 | ] 83 | }, 84 | "version": { 85 | "$id": "/properties/components/items/properties/version", 86 | "type": "string", 87 | "examples": [ 88 | "10.1.1" 89 | ] 90 | }, 91 | "criteria": { 92 | "$id": "/properties/components/items/properties/criteria", 93 | "type": "array", 94 | "items": { 95 | "$id": "/properties/components/items/properties/criteria/items", 96 | "type": "object", 97 | "properties": { 98 | "name": { 99 | "$id": "/properties/components/items/properties/criteria/items/properties/name", 100 | "type": "string", 101 | "examples": [ 102 | "Application Bundle ID" 103 | ] 104 | }, 105 | "operator": { 106 | "$id": "/properties/components/items/properties/criteria/items/properties/operator", 107 | "type": "string", 108 | "examples": [ 109 | "is" 110 | ] 111 | }, 112 | "value": { 113 | "$id": "/properties/components/items/properties/criteria/items/properties/value", 114 | "type": "string", 115 | "examples": [ 116 | "com.jamfsoftware.Composer" 117 | ] 118 | }, 119 | "type": { 120 | "$id": "/properties/components/items/properties/criteria/items/properties/type", 121 | "type": "string", 122 | "examples": [ 123 | "recon" 124 | ] 125 | }, 126 | "and": { 127 | "$id": "/properties/components/items/properties/criteria/items/properties/and", 128 | "type": "boolean", 129 | "examples": [ 130 | true 131 | ] 132 | } 133 | }, 134 | "required": [ 135 | "name", 136 | "operator", 137 | "value", 138 | "type" 139 | ] 140 | } 141 | } 142 | }, 143 | "required": [ 144 | "name", 145 | "version", 146 | "criteria" 147 | ] 148 | } 149 | }, 150 | "capabilities": { 151 | "$id": "/properties/capabilities", 152 | "type": "array", 153 | "items": { 154 | "$id": "/properties/capabilities/items", 155 | "type": "object", 156 | "properties": { 157 | "name": { 158 | "$id": "/properties/capabilities/items/properties/name", 159 | "type": "string", 160 | "examples": [ 161 | "Operating System Version" 162 | ] 163 | }, 164 | "operator": { 165 | "$id": "/properties/capabilities/items/properties/operator", 166 | "type": "string", 167 | "examples": [ 168 | "greater than or equal" 169 | ] 170 | }, 171 | "value": { 172 | "$id": "/properties/capabilities/items/properties/value", 173 | "type": "string", 174 | "examples": [ 175 | "10.9" 176 | ] 177 | }, 178 | "type": { 179 | "$id": "/properties/capabilities/items/properties/type", 180 | "type": "string", 181 | "examples": [ 182 | "recon" 183 | ] 184 | } 185 | }, 186 | "required": [ 187 | "name", 188 | "operator", 189 | "value", 190 | "type" 191 | ] 192 | } 193 | }, 194 | "dependencies": { 195 | "$id": "/properties/dependencies", 196 | "type": "array" 197 | } 198 | }, 199 | "required": [ 200 | "version", 201 | "releaseDate", 202 | "standalone", 203 | "minimumOperatingSystem", 204 | "reboot", 205 | "killApps", 206 | "components", 207 | "capabilities" 208 | ] 209 | } -------------------------------------------------------------------------------- /apis/titles/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Jamf Pro endpoints for Community Patch 4 | 5 | Parameters: 6 | 7 | DomainName: 8 | Type: String 9 | 10 | HostedZoneId: 11 | Type: String 12 | 13 | RegionalCertificateArn: 14 | Type: AWS::SSM::Parameter::Value<String> 15 | 16 | CommunityPatchTableName: 17 | Type: String 18 | 19 | # SAM Globals 20 | 21 | Globals: 22 | Function: 23 | Runtime: python3.7 24 | Handler: index.lambda_handler 25 | Tracing: PassThrough 26 | Environment: 27 | Variables: 28 | COMMUNITY_PATCH_TABLE: !Ref CommunityPatchTableName 29 | DOMAIN_NAME: !Ref DomainName 30 | 31 | Resources: 32 | 33 | # API Gateway 34 | 35 | ApiGateway: 36 | Type: AWS::Serverless::Api 37 | Properties: 38 | StageName: Prod 39 | EndpointConfiguration: REGIONAL 40 | TracingEnabled: true 41 | Auth: 42 | DefaultAuthorizer: ApiAuthorizer 43 | Authorizers: 44 | ApiAuthorizer: 45 | FunctionArn: !GetAtt Authorizer.Arn 46 | 47 | ApiCustomDomain: 48 | Type: AWS::ApiGateway::DomainName 49 | Properties: 50 | DomainName: !Sub 'api.${DomainName}' 51 | RegionalCertificateArn: !Ref RegionalCertificateArn 52 | EndpointConfiguration: 53 | Types: 54 | - REGIONAL 55 | 56 | ApiBasePath: 57 | Type: AWS::ApiGateway::BasePathMapping 58 | Properties: 59 | DomainName: !Ref ApiCustomDomain 60 | RestApiId: !Ref ApiGateway 61 | Stage: Prod 62 | DependsOn: 63 | - ApiGateway 64 | - ApiGatewayProdStage 65 | - ApiCustomDomain 66 | 67 | RegionalRoute53Record: 68 | Type: AWS::Route53::RecordSet 69 | Properties: 70 | Name: !Sub 'api.${DomainName}' 71 | SetIdentifier: !Sub 'patch-api-${AWS::Region}' 72 | AliasTarget: 73 | DNSName: !GetAtt ApiCustomDomain.RegionalDomainName 74 | HostedZoneId: !GetAtt ApiCustomDomain.RegionalHostedZoneId 75 | HostedZoneId: !Ref HostedZoneId 76 | Region: !Ref AWS::Region 77 | Type: A 78 | 79 | # Lambda 80 | 81 | Authorizer: 82 | Type: AWS::Serverless::Function 83 | Properties: 84 | CodeUri: ./src/authorizer 85 | Policies: 86 | - DynamoDBReadPolicy: 87 | TableName: !Ref CommunityPatchTableName 88 | 89 | ReadTitles: 90 | Type: AWS::Serverless::Function 91 | Properties: 92 | CodeUri: ./src/read_titles 93 | Policies: 94 | - DynamoDBReadPolicy: 95 | TableName: !Ref CommunityPatchTableName 96 | Events: 97 | ReadTitles: 98 | Type: Api 99 | Properties: 100 | Path: /v1/titles 101 | Method: get 102 | RestApiId: 103 | Ref: ApiGateway 104 | ReadTitle: 105 | Type: Api 106 | Properties: 107 | Path: /v1/titles/{title_id} 108 | Method: get 109 | RestApiId: 110 | Ref: ApiGateway 111 | 112 | 113 | CreateTitle: 114 | Type: AWS::Serverless::Function 115 | Properties: 116 | CodeUri: ./src/create_title 117 | Policies: 118 | - DynamoDBCrudPolicy: 119 | TableName: !Ref CommunityPatchTableName 120 | Events: 121 | CreateTitle: 122 | Type: Api 123 | Properties: 124 | Path: /v1/titles 125 | Method: post 126 | RestApiId: 127 | Ref: ApiGateway 128 | 129 | UpdateTitleVersion: 130 | Type: AWS::Serverless::Function 131 | Properties: 132 | CodeUri: ./src/update_title_version 133 | Policies: 134 | - DynamoDBCrudPolicy: 135 | TableName: !Ref CommunityPatchTableName 136 | Events: 137 | AddVersion: 138 | Type: Api 139 | Properties: 140 | Path: /v1/titles/{title_id}/versions 141 | Method: post 142 | RestApiId: 143 | Ref: ApiGateway 144 | DeleteVersion: 145 | Type: Api 146 | Properties: 147 | Path: /v1/titles/{title_id}/versions/{version} 148 | Method: delete 149 | RestApiId: 150 | Ref: ApiGateway 151 | 152 | DeleteTitle: 153 | Type: AWS::Serverless::Function 154 | Properties: 155 | CodeUri: ./src/delete_title 156 | Policies: 157 | - DynamoDBCrudPolicy: 158 | TableName: !Ref CommunityPatchTableName 159 | Events: 160 | DeleteTitle: 161 | Type: Api 162 | Properties: 163 | Path: /v1/titles/{title_id} 164 | Method: delete 165 | RestApiId: 166 | Ref: ApiGateway 167 | 168 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | build: 5 | commands: 6 | - | 7 | sam build \ 8 | --region "${AWS_REGION}" \ 9 | --template ${SOURCE_DIR}/template.yaml 10 | post_build: 11 | commands: 12 | - | 13 | for region_name in ${TARGET_REGIONS}; do 14 | sam package --region "${AWS_REGION}" \ 15 | --s3-bucket "${NAMESPACE}-communitypatch-${region_name}-artifacts" \ 16 | --output-template-file "packaged-${region_name}.yaml" 17 | done 18 | artifacts: 19 | type: zip 20 | files: 21 | - packaged-*.yaml 22 | -------------------------------------------------------------------------------- /docs/JamfProSetup.md: -------------------------------------------------------------------------------- 1 | # Jamf Pro Setup 2 | 3 | WIP 4 | -------------------------------------------------------------------------------- /images/AppleID-Sign-In.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/images/AppleID-Sign-In.png -------------------------------------------------------------------------------- /images/CommunityPatch-Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/images/CommunityPatch-Architecture.png -------------------------------------------------------------------------------- /images/Postman-Collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/images/Postman-Collection.png -------------------------------------------------------------------------------- /images/Tokens-Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/images/Tokens-Page.png -------------------------------------------------------------------------------- /pipeline.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | 5 | RepositoryOwner: 6 | Type: String 7 | Default: brysontyrrell 8 | 9 | RepositoryName: 10 | Type: String 11 | Default: CommunityPatch 12 | 13 | SourceBranch: 14 | Type: String 15 | Default: master 16 | 17 | Namespace: 18 | Type: String 19 | Description: Segments CloudFormation stack names 20 | 21 | GitHubToken: 22 | Type: String 23 | Description: Personal access token with 'write:repo_hook' permission. 24 | NoEcho: True 25 | 26 | WebHookSecret: 27 | Type: String 28 | Description: A string of characters to serve as the GitHub webhook signing secret. 29 | NoEcho: True 30 | 31 | DomainName: 32 | Type: String 33 | 34 | HostedZoneId: 35 | Type: String 36 | 37 | Metadata: 38 | 39 | AWS::CloudFormation::Interface: 40 | ParameterGroups: 41 | - Label: 42 | default: GitHub Setup 43 | Parameters: 44 | - RepositoryOwner 45 | - RepositoryName 46 | - SourceBranch 47 | - GitHubToken 48 | - WebHookSecret 49 | - Label: 50 | default: CommunityPatch Setup 51 | Parameters: 52 | - Namespace 53 | - DomainName 54 | - HostedZoneId 55 | 56 | Resources: 57 | 58 | # IAM 59 | 60 | CodeBuildRole: 61 | Type: AWS::IAM::Role 62 | Properties: 63 | AssumeRolePolicyDocument: 64 | Version: 2012-10-17 65 | Statement: 66 | - Effect: Allow 67 | Principal: 68 | Service: 69 | - codebuild.amazonaws.com 70 | Action: 71 | - sts:AssumeRole 72 | Policies: 73 | - PolicyName: CodeBuildPolicy 74 | PolicyDocument: 75 | Version: 2012-10-17 76 | Statement: 77 | - Effect: Allow 78 | Action: 79 | - s3:GetObject* 80 | - s3:PutObject* 81 | Resource: !Sub 'arn:aws:s3:::${Namespace}-communitypatch-*-artifacts/*' 82 | - Effect: Allow 83 | Action: 84 | - logs:CreateLogGroup 85 | - logs:CreateLogStream 86 | - logs:PutLogEvents 87 | Resource: 88 | - arn:aws:logs:*:*:log-group:/aws/codebuild/* 89 | 90 | CodePipelineRole: 91 | Type: AWS::IAM::Role 92 | Properties: 93 | AssumeRolePolicyDocument: 94 | Version: 2012-10-17 95 | Statement: 96 | - Effect: Allow 97 | Principal: 98 | Service: 99 | - codepipeline.amazonaws.com 100 | Action: 101 | - sts:AssumeRole 102 | Policies: 103 | - PolicyName: CodePipelinePolicy 104 | PolicyDocument: 105 | Version: 2012-10-17 106 | Statement: 107 | - Effect: Allow 108 | Action: 109 | - codebuild:* 110 | - cloudformation:* 111 | - cloudwatch:* 112 | Resource: "*" 113 | - Effect: Allow 114 | Action: 115 | - s3:GetObject* 116 | - s3:PutObject* 117 | Resource: !Sub 'arn:aws:s3:::${Namespace}-communitypatch-*-artifacts/*' 118 | 119 | CloudFormationRole: 120 | Type: AWS::IAM::Role 121 | Properties: 122 | AssumeRolePolicyDocument: 123 | Version: 2012-10-17 124 | Statement: 125 | - Effect: Allow 126 | Principal: 127 | Service: 128 | - cloudformation.amazonaws.com 129 | AWS: 130 | - !GetAtt CodePipelineRole.Arn 131 | Action: 132 | - sts:AssumeRole 133 | ManagedPolicyArns: 134 | - arn:aws:iam::aws:policy/AdministratorAccess 135 | 136 | # CodeBuild 137 | 138 | BuildProject: 139 | Type: AWS::CodeBuild::Project 140 | Properties: 141 | Artifacts: 142 | Type: CODEPIPELINE 143 | EncryptionDisabled: True 144 | BadgeEnabled: False 145 | Environment: 146 | ComputeType: BUILD_GENERAL1_SMALL 147 | Image: lambci/lambda:build-python3.7 148 | Type: LINUX_CONTAINER 149 | EnvironmentVariables: 150 | - Type: PLAINTEXT 151 | Name: NAMESPACE 152 | Value: !Ref Namespace 153 | - Type: PLAINTEXT 154 | Name: TARGET_REGIONS 155 | Value: 'us-east-2 eu-central-1 ap-southeast-2' 156 | LogsConfig: 157 | CloudWatchLogs: 158 | Status: ENABLED 159 | TimeoutInMinutes: 20 160 | QueuedTimeoutInMinutes: 180 161 | ServiceRole: !GetAtt CodeBuildRole.Arn 162 | Source: 163 | Type: CODEPIPELINE 164 | 165 | # CodePipeline 166 | 167 | Pipeline: 168 | Type: AWS::CodePipeline::Pipeline 169 | Properties: 170 | RoleArn: !GetAtt CodePipelineRole.Arn 171 | 172 | ArtifactStores: 173 | - ArtifactStore: 174 | Type: S3 175 | Location: !Sub '${Namespace}-communitypatch-us-east-1-artifacts' 176 | Region: us-east-1 177 | - ArtifactStore: 178 | Type: S3 179 | Location: !Sub '${Namespace}-communitypatch-us-east-2-artifacts' 180 | Region: us-east-2 181 | - ArtifactStore: 182 | Type: S3 183 | Location: !Sub '${Namespace}-communitypatch-eu-central-1-artifacts' 184 | Region: eu-central-1 185 | - ArtifactStore: 186 | Type: S3 187 | Location: !Sub '${Namespace}-communitypatch-ap-southeast-2-artifacts' 188 | Region: ap-southeast-2 189 | 190 | Stages: 191 | - Name: Source 192 | Actions: 193 | - Name: GitHubCheckout 194 | ActionTypeId: 195 | Category: Source 196 | Owner: ThirdParty 197 | Provider: GitHub 198 | Version: 1 199 | Configuration: 200 | Owner: !Ref RepositoryOwner 201 | Repo: !Ref RepositoryName 202 | Branch: !Ref SourceBranch 203 | PollForSourceChanges: False 204 | OAuthToken: !Ref GitHubToken 205 | OutputArtifacts: 206 | - Name: SourceArtifact 207 | Region: !Ref AWS::Region 208 | RunOrder: 1 209 | 210 | - Name: Build 211 | Actions: 212 | - Name: Resources 213 | InputArtifacts: 214 | - Name: SourceArtifact 215 | ActionTypeId: 216 | Category: Build 217 | Owner: AWS 218 | Provider: CodeBuild 219 | Version: 1 220 | OutputArtifacts: 221 | - Name: ResourcesBuildArtifact 222 | Configuration: 223 | ProjectName: !Ref BuildProject 224 | EnvironmentVariables: > 225 | [ 226 | { 227 | "name": "SOURCE_DIR", 228 | "type": "PLAINTEXT", 229 | "value": "resources/regional" 230 | } 231 | ] 232 | RunOrder: 1 233 | 234 | - Name: Contributors-API 235 | InputArtifacts: 236 | - Name: SourceArtifact 237 | ActionTypeId: 238 | Category: Build 239 | Owner: AWS 240 | Provider: CodeBuild 241 | Version: 1 242 | OutputArtifacts: 243 | - Name: ContributorsApiBuildArtifact 244 | Configuration: 245 | ProjectName: !Ref BuildProject 246 | EnvironmentVariables: > 247 | [ 248 | { 249 | "name": "SOURCE_DIR", 250 | "type": "PLAINTEXT", 251 | "value": "apis/contributors" 252 | } 253 | ] 254 | RunOrder: 1 255 | 256 | - Name: Titles-API 257 | InputArtifacts: 258 | - Name: SourceArtifact 259 | ActionTypeId: 260 | Category: Build 261 | Owner: AWS 262 | Provider: CodeBuild 263 | Version: 1 264 | OutputArtifacts: 265 | - Name: TitlesApiBuildArtifact 266 | Configuration: 267 | ProjectName: !Ref BuildProject 268 | EnvironmentVariables: > 269 | [ 270 | { 271 | "name": "SOURCE_DIR", 272 | "type": "PLAINTEXT", 273 | "value": "apis/titles" 274 | } 275 | ] 276 | RunOrder: 1 277 | 278 | - Name: Jamf-API 279 | InputArtifacts: 280 | - Name: SourceArtifact 281 | ActionTypeId: 282 | Category: Build 283 | Owner: AWS 284 | Provider: CodeBuild 285 | Version: 1 286 | OutputArtifacts: 287 | - Name: JamfApiBuildArtifact 288 | Configuration: 289 | ProjectName: !Ref BuildProject 290 | EnvironmentVariables: > 291 | [ 292 | { 293 | "name": "SOURCE_DIR", 294 | "type": "PLAINTEXT", 295 | "value": "apis/jamf" 296 | } 297 | ] 298 | RunOrder: 1 299 | 300 | - Name: Resources 301 | Actions: 302 | - Name: US1-Cognito 303 | Region: us-east-1 304 | InputArtifacts: 305 | - Name: SourceArtifact 306 | OutputArtifacts: 307 | - Name: CognitoArtifact 308 | ActionTypeId: 309 | Category: Deploy 310 | Owner: AWS 311 | Provider: CloudFormation 312 | Version: 1 313 | RoleArn: !GetAtt CloudFormationRole.Arn 314 | Configuration: 315 | ActionMode: CREATE_UPDATE 316 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 317 | RoleArn: !GetAtt CloudFormationRole.Arn 318 | StackName: !Sub ${Namespace}-communitypatch-global 319 | TemplatePath: 'SourceArtifact::resources/global/cognito.yaml' 320 | ParameterOverrides: !Sub | 321 | { 322 | "DomainName": "${DomainName}", 323 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn" 324 | } 325 | OutputFileName: outputs.json 326 | RunOrder: 1 327 | 328 | - Name: US2-Global-Tables 329 | Region: us-east-2 330 | InputArtifacts: 331 | - Name: SourceArtifact 332 | OutputArtifacts: 333 | - Name: GlobalTablesArtifact 334 | ActionTypeId: 335 | Category: Deploy 336 | Owner: AWS 337 | Provider: CloudFormation 338 | Version: 1 339 | RoleArn: !GetAtt CloudFormationRole.Arn 340 | Configuration: 341 | ActionMode: CREATE_UPDATE 342 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 343 | RoleArn: !GetAtt CloudFormationRole.Arn 344 | StackName: !Sub ${Namespace}-communitypatch-global-tables 345 | TemplatePath: 'SourceArtifact::resources/global/tables.yaml' 346 | OutputFileName: outputs.json 347 | RunOrder: 1 348 | 349 | - Name: US2-Resources 350 | Region: us-east-2 351 | InputArtifacts: 352 | - Name: ResourcesBuildArtifact 353 | - Name: GlobalTablesArtifact 354 | OutputArtifacts: 355 | - Name: USResourcesArtifact 356 | ActionTypeId: 357 | Category: Deploy 358 | Owner: AWS 359 | Provider: CloudFormation 360 | Version: 1 361 | RoleArn: !GetAtt CloudFormationRole.Arn 362 | Configuration: 363 | ActionMode: CREATE_UPDATE 364 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 365 | RoleArn: !GetAtt CloudFormationRole.Arn 366 | StackName: !Sub ${Namespace}-communitypatch-resources 367 | TemplatePath: 'ResourcesBuildArtifact::packaged-us-east-2.yaml' 368 | ParameterOverrides: !Sub | 369 | { 370 | "Namespace": "${Namespace}", 371 | "DomainName": "${DomainName}", 372 | "HostedZoneId": "${HostedZoneId}", 373 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 374 | } 375 | OutputFileName: outputs.json 376 | RunOrder: 2 377 | 378 | - Name: EU-Resources 379 | Region: eu-central-1 380 | InputArtifacts: 381 | - Name: ResourcesBuildArtifact 382 | - Name: GlobalTablesArtifact 383 | OutputArtifacts: 384 | - Name: EUResourcesArtifact 385 | ActionTypeId: 386 | Category: Deploy 387 | Owner: AWS 388 | Provider: CloudFormation 389 | Version: 1 390 | RoleArn: !GetAtt CloudFormationRole.Arn 391 | Configuration: 392 | ActionMode: CREATE_UPDATE 393 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 394 | RoleArn: !GetAtt CloudFormationRole.Arn 395 | StackName: !Sub ${Namespace}-communitypatch-resources 396 | TemplatePath: 'ResourcesBuildArtifact::packaged-eu-central-1.yaml' 397 | ParameterOverrides: !Sub | 398 | { 399 | "Namespace": "${Namespace}", 400 | "DomainName": "${DomainName}", 401 | "HostedZoneId": "${HostedZoneId}", 402 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 403 | } 404 | OutputFileName: outputs.json 405 | RunOrder: 2 406 | 407 | - Name: AUS-Resources 408 | Region: ap-southeast-2 409 | InputArtifacts: 410 | - Name: ResourcesBuildArtifact 411 | - Name: GlobalTablesArtifact 412 | OutputArtifacts: 413 | - Name: AUSResourcesArtifact 414 | ActionTypeId: 415 | Category: Deploy 416 | Owner: AWS 417 | Provider: CloudFormation 418 | Version: 1 419 | RoleArn: !GetAtt CloudFormationRole.Arn 420 | Configuration: 421 | ActionMode: CREATE_UPDATE 422 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 423 | RoleArn: !GetAtt CloudFormationRole.Arn 424 | StackName: !Sub ${Namespace}-communitypatch-resources 425 | TemplatePath: 'ResourcesBuildArtifact::packaged-ap-southeast-2.yaml' 426 | ParameterOverrides: !Sub | 427 | { 428 | "Namespace": "${Namespace}", 429 | "DomainName": "${DomainName}", 430 | "HostedZoneId": "${HostedZoneId}", 431 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 432 | } 433 | OutputFileName: outputs.json 434 | RunOrder: 2 435 | 436 | - Name: Deploy-API-Stacks 437 | Actions: 438 | - Name: US2-Contributors-API 439 | Region: us-east-2 440 | InputArtifacts: 441 | - Name: ContributorsApiBuildArtifact 442 | - Name: CognitoArtifact 443 | - Name: GlobalTablesArtifact 444 | ActionTypeId: 445 | Category: Deploy 446 | Owner: AWS 447 | Provider: CloudFormation 448 | Version: 1 449 | RoleArn: !GetAtt CloudFormationRole.Arn 450 | Configuration: 451 | ActionMode: CREATE_UPDATE 452 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 453 | RoleArn: !GetAtt CloudFormationRole.Arn 454 | StackName: !Sub ${Namespace}-communitypatch-api-contributors 455 | TemplatePath: 'ContributorsApiBuildArtifact::packaged-us-east-2.yaml' 456 | ParameterOverrides: !Sub | 457 | { 458 | "Namespace": "${Namespace}", 459 | "DomainName": "${DomainName}", 460 | "HostedZoneId": "${HostedZoneId}", 461 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 462 | "CognitoUserPoolArn": { "Fn::GetParam" : ["CognitoArtifact", "outputs.json", "CognitoUserPoolArn"]}, 463 | "AppleClientId": { "Fn::GetParam" : ["CognitoArtifact", "outputs.json", "AppleIdLoginClientId"]}, 464 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 465 | } 466 | RunOrder: 1 467 | 468 | - Name: EU-Contributors-API 469 | Region: eu-central-1 470 | InputArtifacts: 471 | - Name: ContributorsApiBuildArtifact 472 | - Name: CognitoArtifact 473 | - Name: GlobalTablesArtifact 474 | ActionTypeId: 475 | Category: Deploy 476 | Owner: AWS 477 | Provider: CloudFormation 478 | Version: 1 479 | RoleArn: !GetAtt CloudFormationRole.Arn 480 | Configuration: 481 | ActionMode: CREATE_UPDATE 482 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 483 | RoleArn: !GetAtt CloudFormationRole.Arn 484 | StackName: !Sub ${Namespace}-communitypatch-api-contributors 485 | TemplatePath: 'ContributorsApiBuildArtifact::packaged-eu-central-1.yaml' 486 | ParameterOverrides: !Sub | 487 | { 488 | "Namespace": "${Namespace}", 489 | "DomainName": "${DomainName}", 490 | "HostedZoneId": "${HostedZoneId}", 491 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 492 | "CognitoUserPoolArn": { "Fn::GetParam" : ["CognitoArtifact", "outputs.json", "CognitoUserPoolArn"]}, 493 | "AppleClientId": { "Fn::GetParam" : ["CognitoArtifact", "outputs.json", "AppleIdLoginClientId"]}, 494 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 495 | } 496 | RunOrder: 1 497 | 498 | - Name: AUS-Contributors-API 499 | Region: ap-southeast-2 500 | InputArtifacts: 501 | - Name: ContributorsApiBuildArtifact 502 | - Name: CognitoArtifact 503 | - Name: GlobalTablesArtifact 504 | ActionTypeId: 505 | Category: Deploy 506 | Owner: AWS 507 | Provider: CloudFormation 508 | Version: 1 509 | RoleArn: !GetAtt CloudFormationRole.Arn 510 | Configuration: 511 | ActionMode: CREATE_UPDATE 512 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 513 | RoleArn: !GetAtt CloudFormationRole.Arn 514 | StackName: !Sub ${Namespace}-communitypatch-api-contributors 515 | TemplatePath: 'ContributorsApiBuildArtifact::packaged-ap-southeast-2.yaml' 516 | ParameterOverrides: !Sub | 517 | { 518 | "Namespace": "${Namespace}", 519 | "DomainName": "${DomainName}", 520 | "HostedZoneId": "${HostedZoneId}", 521 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 522 | "CognitoUserPoolArn": { "Fn::GetParam" : ["CognitoArtifact", "outputs.json", "CognitoUserPoolArn"]}, 523 | "AppleClientId": { "Fn::GetParam" : ["CognitoArtifact", "outputs.json", "AppleIdLoginClientId"]}, 524 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 525 | } 526 | RunOrder: 1 527 | 528 | 529 | - Name: US2-Titles-API 530 | Region: us-east-2 531 | InputArtifacts: 532 | - Name: TitlesApiBuildArtifact 533 | - Name: CognitoArtifact 534 | - Name: GlobalTablesArtifact 535 | ActionTypeId: 536 | Category: Deploy 537 | Owner: AWS 538 | Provider: CloudFormation 539 | Version: 1 540 | RoleArn: !GetAtt CloudFormationRole.Arn 541 | Configuration: 542 | ActionMode: CREATE_UPDATE 543 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 544 | RoleArn: !GetAtt CloudFormationRole.Arn 545 | StackName: !Sub ${Namespace}-communitypatch-api-titles 546 | TemplatePath: 'TitlesApiBuildArtifact::packaged-us-east-2.yaml' 547 | ParameterOverrides: !Sub | 548 | { 549 | "DomainName": "${DomainName}", 550 | "HostedZoneId": "${HostedZoneId}", 551 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 552 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 553 | } 554 | RunOrder: 2 555 | 556 | - Name: EU-Titles-API 557 | Region: eu-central-1 558 | InputArtifacts: 559 | - Name: TitlesApiBuildArtifact 560 | - Name: CognitoArtifact 561 | - Name: GlobalTablesArtifact 562 | ActionTypeId: 563 | Category: Deploy 564 | Owner: AWS 565 | Provider: CloudFormation 566 | Version: 1 567 | RoleArn: !GetAtt CloudFormationRole.Arn 568 | Configuration: 569 | ActionMode: CREATE_UPDATE 570 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 571 | RoleArn: !GetAtt CloudFormationRole.Arn 572 | StackName: !Sub ${Namespace}-communitypatch-api-titles 573 | TemplatePath: 'TitlesApiBuildArtifact::packaged-eu-central-1.yaml' 574 | ParameterOverrides: !Sub | 575 | { 576 | "DomainName": "${DomainName}", 577 | "HostedZoneId": "${HostedZoneId}", 578 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 579 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 580 | } 581 | RunOrder: 2 582 | 583 | - Name: AUS-Titles-API 584 | Region: ap-southeast-2 585 | InputArtifacts: 586 | - Name: TitlesApiBuildArtifact 587 | - Name: CognitoArtifact 588 | - Name: GlobalTablesArtifact 589 | ActionTypeId: 590 | Category: Deploy 591 | Owner: AWS 592 | Provider: CloudFormation 593 | Version: 1 594 | RoleArn: !GetAtt CloudFormationRole.Arn 595 | Configuration: 596 | ActionMode: CREATE_UPDATE 597 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 598 | RoleArn: !GetAtt CloudFormationRole.Arn 599 | StackName: !Sub ${Namespace}-communitypatch-api-titles 600 | TemplatePath: 'TitlesApiBuildArtifact::packaged-ap-southeast-2.yaml' 601 | ParameterOverrides: !Sub | 602 | { 603 | "DomainName": "${DomainName}", 604 | "HostedZoneId": "${HostedZoneId}", 605 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 606 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 607 | } 608 | RunOrder: 2 609 | 610 | - Name: US2-Jamf-API 611 | Region: us-east-2 612 | InputArtifacts: 613 | - Name: JamfApiBuildArtifact 614 | - Name: CognitoArtifact 615 | - Name: GlobalTablesArtifact 616 | ActionTypeId: 617 | Category: Deploy 618 | Owner: AWS 619 | Provider: CloudFormation 620 | Version: 1 621 | RoleArn: !GetAtt CloudFormationRole.Arn 622 | Configuration: 623 | ActionMode: CREATE_UPDATE 624 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 625 | RoleArn: !GetAtt CloudFormationRole.Arn 626 | StackName: !Sub ${Namespace}-communitypatch-api-jamf 627 | TemplatePath: 'JamfApiBuildArtifact::packaged-us-east-2.yaml' 628 | ParameterOverrides: !Sub | 629 | { 630 | "DomainName": "${DomainName}", 631 | "HostedZoneId": "${HostedZoneId}", 632 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 633 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 634 | } 635 | RunOrder: 3 636 | 637 | - Name: EU-Jamf-API 638 | Region: eu-central-1 639 | InputArtifacts: 640 | - Name: JamfApiBuildArtifact 641 | - Name: CognitoArtifact 642 | - Name: GlobalTablesArtifact 643 | ActionTypeId: 644 | Category: Deploy 645 | Owner: AWS 646 | Provider: CloudFormation 647 | Version: 1 648 | RoleArn: !GetAtt CloudFormationRole.Arn 649 | Configuration: 650 | ActionMode: CREATE_UPDATE 651 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 652 | RoleArn: !GetAtt CloudFormationRole.Arn 653 | StackName: !Sub ${Namespace}-communitypatch-api-jamf 654 | TemplatePath: 'JamfApiBuildArtifact::packaged-eu-central-1.yaml' 655 | ParameterOverrides: !Sub | 656 | { 657 | "DomainName": "${DomainName}", 658 | "HostedZoneId": "${HostedZoneId}", 659 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 660 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 661 | } 662 | RunOrder: 3 663 | 664 | - Name: AUS-Jamf-API 665 | Region: ap-southeast-2 666 | InputArtifacts: 667 | - Name: JamfApiBuildArtifact 668 | - Name: CognitoArtifact 669 | - Name: GlobalTablesArtifact 670 | ActionTypeId: 671 | Category: Deploy 672 | Owner: AWS 673 | Provider: CloudFormation 674 | Version: 1 675 | RoleArn: !GetAtt CloudFormationRole.Arn 676 | Configuration: 677 | ActionMode: CREATE_UPDATE 678 | Capabilities: CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND 679 | RoleArn: !GetAtt CloudFormationRole.Arn 680 | StackName: !Sub ${Namespace}-communitypatch-api-jamf 681 | TemplatePath: 'JamfApiBuildArtifact::packaged-ap-southeast-2.yaml' 682 | ParameterOverrides: !Sub | 683 | { 684 | "DomainName": "${DomainName}", 685 | "HostedZoneId": "${HostedZoneId}", 686 | "RegionalCertificateArn": "/communitypatch/${Namespace}/certificate_arn", 687 | "CommunityPatchTableName": { "Fn::GetParam" : ["GlobalTablesArtifact", "outputs.json", "CommunityPatchTableName"]} 688 | } 689 | RunOrder: 3 690 | 691 | PipelineWebHook: 692 | Type: AWS::CodePipeline::Webhook 693 | Properties: 694 | Filters: 695 | - JsonPath: '$.ref' 696 | MatchEquals: 'refs/heads/{Branch}' 697 | Authentication: GITHUB_HMAC 698 | AuthenticationConfiguration: 699 | SecretToken: !Base64 700 | Ref: WebHookSecret 701 | TargetPipeline: !Ref Pipeline 702 | TargetAction: GitHubCheckout 703 | TargetPipelineVersion: 1 704 | RegisterWithThirdParty: False 705 | -------------------------------------------------------------------------------- /postman-collections/CommunityPatch.Dev.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "8d9e3c2d-2964-4a3d-9afb-2271c42474e5", 4 | "name": "CommunityPatch.Dev", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Contributors API", 10 | "item": [ 11 | { 12 | "name": "Create API Token", 13 | "event": [ 14 | { 15 | "listen": "test", 16 | "script": { 17 | "id": "95e49ad1-adb5-4c83-b223-43a2e429bd6e", 18 | "exec": [ 19 | "var jsonData = JSON.parse(responseBody);", 20 | "postman.setEnvironmentVariable(\"API Token ID\", jsonData.id);", 21 | "postman.setEnvironmentVariable(\"API Token\", jsonData.api_token);", 22 | "" 23 | ], 24 | "type": "text/javascript" 25 | } 26 | } 27 | ], 28 | "request": { 29 | "method": "POST", 30 | "header": [ 31 | { 32 | "key": "Authorization", 33 | "value": "{{Access Token}}", 34 | "type": "text" 35 | } 36 | ], 37 | "url": { 38 | "raw": "https://contributors.communitypatch.dev/v1/tokens", 39 | "protocol": "https", 40 | "host": [ 41 | "contributors", 42 | "communitypatch", 43 | "dev" 44 | ], 45 | "path": [ 46 | "v1", 47 | "tokens" 48 | ] 49 | } 50 | }, 51 | "response": [] 52 | }, 53 | { 54 | "name": "Create API Token (With Options)", 55 | "event": [ 56 | { 57 | "listen": "test", 58 | "script": { 59 | "id": "95e49ad1-adb5-4c83-b223-43a2e429bd6e", 60 | "exec": [ 61 | "var jsonData = JSON.parse(responseBody);", 62 | "postman.setEnvironmentVariable(\"API Token ID\", jsonData.id);", 63 | "postman.setEnvironmentVariable(\"API Token\", jsonData.api_token);", 64 | "" 65 | ], 66 | "type": "text/javascript" 67 | } 68 | } 69 | ], 70 | "request": { 71 | "method": "POST", 72 | "header": [ 73 | { 74 | "key": "Authorization", 75 | "type": "text", 76 | "value": "{{Access Token}}" 77 | }, 78 | { 79 | "key": "Content-Type", 80 | "value": "application/json", 81 | "type": "text" 82 | } 83 | ], 84 | "body": { 85 | "mode": "raw", 86 | "raw": "{\n\t\"expires_in_days\": 1,\n\t\"titles_in_scope\": []\n}" 87 | }, 88 | "url": { 89 | "raw": "https://contributors.communitypatch.dev/v1/tokens", 90 | "protocol": "https", 91 | "host": [ 92 | "contributors", 93 | "communitypatch", 94 | "dev" 95 | ], 96 | "path": [ 97 | "v1", 98 | "tokens" 99 | ] 100 | } 101 | }, 102 | "response": [] 103 | }, 104 | { 105 | "name": "Invalidate API Token", 106 | "event": [ 107 | { 108 | "listen": "test", 109 | "script": { 110 | "id": "91eabfb5-3f82-481b-8864-7ee2abc9b986", 111 | "exec": [ 112 | "var jsonData = JSON.parse(responseBody);", 113 | "postman.setEnvironmentVariable(\"API Token ID\", \"\");", 114 | "postman.setEnvironmentVariable(\"API Token\", \"\");", 115 | "" 116 | ], 117 | "type": "text/javascript" 118 | } 119 | } 120 | ], 121 | "request": { 122 | "method": "POST", 123 | "header": [ 124 | { 125 | "key": "Authorization", 126 | "value": "{{Access Token}}", 127 | "type": "text" 128 | } 129 | ], 130 | "url": { 131 | "raw": "https://contributors.communitypatch.dev/v1/tokens/:token_id/invalidate", 132 | "protocol": "https", 133 | "host": [ 134 | "contributors", 135 | "communitypatch", 136 | "dev" 137 | ], 138 | "path": [ 139 | "v1", 140 | "tokens", 141 | ":token_id", 142 | "invalidate" 143 | ], 144 | "variable": [ 145 | { 146 | "key": "token_id", 147 | "value": "{{API Token}}" 148 | } 149 | ] 150 | } 151 | }, 152 | "response": [] 153 | } 154 | ], 155 | "protocolProfileBehavior": {} 156 | }, 157 | { 158 | "name": "Titles API", 159 | "item": [ 160 | { 161 | "name": "Create Title", 162 | "request": { 163 | "method": "POST", 164 | "header": [ 165 | { 166 | "key": "Authorization", 167 | "type": "text", 168 | "value": "{{API Token}}" 169 | }, 170 | { 171 | "key": "Content-Type", 172 | "value": "application/json", 173 | "type": "text" 174 | } 175 | ], 176 | "body": { 177 | "mode": "raw", 178 | "raw": "{\n \"publisher\": \"Xcode\", \n \"currentVersion\": \"11.2.1\", \n \"requirements\": [\n {\n \"operator\": \"is\", \n \"and\": true, \n \"type\": \"recon\", \n \"name\": \"Application Bundle ID\", \n \"value\": \"com.apple.dt.Xcode\"\n }\n ], \n \"name\": \"Xcode\", \n \"appName\": \"Xcode.app\", \n \"lastModified\": \"2020-03-10T20:42:08Z\", \n \"patches\": [\n {\n \"releaseDate\": \"2019-12-09T19:55:53Z\", \n \"killApps\": [\n {\n \"appName\": \"Xcode.app\", \n \"bundleId\": \"com.apple.dt.Xcode\"\n }\n ], \n \"version\": \"11.2.1\", \n \"components\": [\n {\n \"version\": \"11.2.1\", \n \"name\": \"Xcode\", \n \"criteria\": [\n {\n \"operator\": \"is\", \n \"and\": true, \n \"type\": \"recon\", \n \"name\": \"Application Bundle ID\", \n \"value\": \"com.apple.dt.Xcode\"\n }, \n {\n \"operator\": \"is\", \n \"type\": \"recon\", \n \"name\": \"Application Version\", \n \"value\": \"11.2.1\"\n }\n ]\n }\n ], \n \"standalone\": true, \n \"minimumOperatingSystem\": \"10.14.4\", \n \"dependencies\": [], \n \"reboot\": false, \n \"capabilities\": [\n {\n \"operator\": \"greater than or equal\", \n \"type\": \"recon\", \n \"name\": \"Operating System Version\", \n \"value\": \"10.14.4\"\n }\n ]\n }\n ], \n \"extensionAttributes\": [], \n \"id\": \"Xcode\", \n \"bundleId\": \"com.apple.dt.Xcode\"\n}" 179 | }, 180 | "url": { 181 | "raw": "https://api.communitypatch.dev/v1/titles", 182 | "protocol": "https", 183 | "host": [ 184 | "api", 185 | "communitypatch", 186 | "dev" 187 | ], 188 | "path": [ 189 | "v1", 190 | "titles" 191 | ] 192 | } 193 | }, 194 | "response": [] 195 | }, 196 | { 197 | "name": "Add Version", 198 | "request": { 199 | "method": "POST", 200 | "header": [ 201 | { 202 | "key": "Authorization", 203 | "value": "{{API Token}}", 204 | "type": "text" 205 | }, 206 | { 207 | "key": "Content-Type", 208 | "value": "application/json", 209 | "type": "text" 210 | } 211 | ], 212 | "body": { 213 | "mode": "raw", 214 | "raw": "{\n \"releaseDate\": \"2020-03-10T21:17:16Z\", \n \"killApps\": [\n {\n \"appName\": \"Xcode.app\", \n \"bundleId\": \"com.apple.dt.Xcode\"\n }\n ], \n \"version\": \"11.3.1\", \n \"components\": [\n {\n \"version\": \"11.3.1\", \n \"name\": \"Xcode\", \n \"criteria\": [\n {\n \"operator\": \"is\", \n \"and\": true, \n \"type\": \"recon\", \n \"name\": \"Application Bundle ID\", \n \"value\": \"com.apple.dt.Xcode\"\n }, \n {\n \"operator\": \"is\", \n \"type\": \"recon\", \n \"name\": \"Application Version\", \n \"value\": \"11.3.1\"\n }\n ]\n }\n ], \n \"standalone\": true, \n \"minimumOperatingSystem\": \"10.14.4\", \n \"dependencies\": [], \n \"reboot\": false, \n \"capabilities\": [\n {\n \"operator\": \"greater than or equal\", \n \"type\": \"recon\", \n \"name\": \"Operating System Version\", \n \"value\": \"10.14.4\"\n }\n ]\n}" 215 | }, 216 | "url": { 217 | "raw": "https://api.communitypatch.dev/v1/titles/Xcode/versions", 218 | "protocol": "https", 219 | "host": [ 220 | "api", 221 | "communitypatch", 222 | "dev" 223 | ], 224 | "path": [ 225 | "v1", 226 | "titles", 227 | "Xcode", 228 | "versions" 229 | ] 230 | } 231 | }, 232 | "response": [] 233 | }, 234 | { 235 | "name": "Delete Version", 236 | "request": { 237 | "method": "DELETE", 238 | "header": [ 239 | { 240 | "key": "Authorization", 241 | "value": "{{API Token}}", 242 | "type": "text" 243 | } 244 | ], 245 | "url": { 246 | "raw": "https://api.communitypatch.dev/v1/titles/Xcode/versions/:version", 247 | "protocol": "https", 248 | "host": [ 249 | "api", 250 | "communitypatch", 251 | "dev" 252 | ], 253 | "path": [ 254 | "v1", 255 | "titles", 256 | "Xcode", 257 | "versions", 258 | ":version" 259 | ], 260 | "variable": [ 261 | { 262 | "key": "version", 263 | "value": "" 264 | } 265 | ] 266 | } 267 | }, 268 | "response": [] 269 | }, 270 | { 271 | "name": "Read All Titles", 272 | "request": { 273 | "method": "GET", 274 | "header": [ 275 | { 276 | "key": "Authorization", 277 | "value": "{{API Token}}", 278 | "type": "text" 279 | }, 280 | { 281 | "key": "Accept", 282 | "value": "application/json", 283 | "type": "text" 284 | } 285 | ], 286 | "url": { 287 | "raw": "https://api.communitypatch.dev/v1/titles/:title_id", 288 | "protocol": "https", 289 | "host": [ 290 | "api", 291 | "communitypatch", 292 | "dev" 293 | ], 294 | "path": [ 295 | "v1", 296 | "titles", 297 | ":title_id" 298 | ], 299 | "variable": [ 300 | { 301 | "key": "title_id", 302 | "value": "" 303 | } 304 | ] 305 | } 306 | }, 307 | "response": [] 308 | }, 309 | { 310 | "name": "Read Title", 311 | "request": { 312 | "method": "GET", 313 | "header": [ 314 | { 315 | "key": "Authorization", 316 | "value": "{{API Token}}", 317 | "type": "text" 318 | }, 319 | { 320 | "key": "Accept", 321 | "value": "application/json", 322 | "type": "text" 323 | } 324 | ], 325 | "url": { 326 | "raw": "https://api.communitypatch.dev/v1/titles/:title_id", 327 | "protocol": "https", 328 | "host": [ 329 | "api", 330 | "communitypatch", 331 | "dev" 332 | ], 333 | "path": [ 334 | "v1", 335 | "titles", 336 | ":title_id" 337 | ], 338 | "variable": [ 339 | { 340 | "key": "title_id", 341 | "value": "" 342 | } 343 | ] 344 | } 345 | }, 346 | "response": [] 347 | }, 348 | { 349 | "name": "Delete Title", 350 | "request": { 351 | "method": "DELETE", 352 | "header": [ 353 | { 354 | "key": "Authorization", 355 | "value": "{{API Token}}", 356 | "type": "text" 357 | } 358 | ], 359 | "url": { 360 | "raw": "https://api.communitypatch.dev/api/v1/titles/:title_id", 361 | "protocol": "https", 362 | "host": [ 363 | "api", 364 | "communitypatch", 365 | "dev" 366 | ], 367 | "path": [ 368 | "api", 369 | "v1", 370 | "titles", 371 | ":title_id" 372 | ], 373 | "variable": [ 374 | { 375 | "key": "title_id", 376 | "value": "" 377 | } 378 | ] 379 | } 380 | }, 381 | "response": [] 382 | } 383 | ], 384 | "protocolProfileBehavior": {} 385 | }, 386 | { 387 | "name": "Jamf API", 388 | "item": [ 389 | { 390 | "name": "Read All Title Summaries", 391 | "request": { 392 | "method": "GET", 393 | "header": [ 394 | { 395 | "key": "Accept", 396 | "value": "application/json", 397 | "type": "text" 398 | } 399 | ], 400 | "url": { 401 | "raw": "https://jamf.communitypatch.dev/v1/:contributor_id/software", 402 | "protocol": "https", 403 | "host": [ 404 | "jamf", 405 | "communitypatch", 406 | "dev" 407 | ], 408 | "path": [ 409 | "v1", 410 | ":contributor_id", 411 | "software" 412 | ], 413 | "variable": [ 414 | { 415 | "key": "contributor_id", 416 | "value": "{{Contributor ID}}" 417 | } 418 | ] 419 | } 420 | }, 421 | "response": [] 422 | }, 423 | { 424 | "name": "Read Selected Title Summaries", 425 | "request": { 426 | "method": "GET", 427 | "header": [ 428 | { 429 | "key": "Accept", 430 | "value": "application/json", 431 | "type": "text" 432 | } 433 | ], 434 | "url": { 435 | "raw": "https://jamf.communitypatch.dev/v1/:contributor_id/software/:title_id", 436 | "protocol": "https", 437 | "host": [ 438 | "jamf", 439 | "communitypatch", 440 | "dev" 441 | ], 442 | "path": [ 443 | "v1", 444 | ":contributor_id", 445 | "software", 446 | ":title_id" 447 | ], 448 | "variable": [ 449 | { 450 | "key": "contributor_id", 451 | "value": "{{Contributor ID}}" 452 | }, 453 | { 454 | "key": "title_id", 455 | "value": "" 456 | } 457 | ] 458 | } 459 | }, 460 | "response": [] 461 | }, 462 | { 463 | "name": "Read Title Definition", 464 | "request": { 465 | "method": "GET", 466 | "header": [ 467 | { 468 | "key": "Accept", 469 | "value": "application/json", 470 | "type": "text" 471 | } 472 | ], 473 | "url": { 474 | "raw": "https://jamf.communitypatch.dev/v1/:contributor_id/patch/macOS", 475 | "protocol": "https", 476 | "host": [ 477 | "jamf", 478 | "communitypatch", 479 | "dev" 480 | ], 481 | "path": [ 482 | "v1", 483 | ":contributor_id", 484 | "patch", 485 | "macOS" 486 | ], 487 | "variable": [ 488 | { 489 | "key": "contributor_id", 490 | "value": "{{Contributor ID}}" 491 | } 492 | ] 493 | } 494 | }, 495 | "response": [] 496 | } 497 | ], 498 | "protocolProfileBehavior": {} 499 | } 500 | ], 501 | "protocolProfileBehavior": {} 502 | } -------------------------------------------------------------------------------- /postman-collections/CommunityPatch.Dev.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "780bc35d-8e4d-433e-b9f7-45663645eea4", 3 | "name": "CommunityPatch.Dev", 4 | "values": [ 5 | { 6 | "key": "Access Token", 7 | "value": "", 8 | "enabled": true 9 | }, 10 | { 11 | "key": "Contributor ID", 12 | "value": "", 13 | "enabled": true 14 | }, 15 | { 16 | "key": "API Token", 17 | "value": "", 18 | "enabled": true 19 | }, 20 | { 21 | "key": "API Token ID", 22 | "value": "", 23 | "enabled": true 24 | } 25 | ], 26 | "_postman_variable_scope": "environment", 27 | "_postman_exported_at": "2020-03-10T21:38:33.705Z", 28 | "_postman_exported_using": "Postman/7.20.0" 29 | } -------------------------------------------------------------------------------- /postman-collections/Postman.md: -------------------------------------------------------------------------------- 1 | # Postman 2 | 3 | These JSON files can be imported into Postman to explore the CommunityPatch APIs without having to use any code. 4 | 5 | ![Postman Collection Screenshot](../images/Postman-Collection.png) 6 | 7 | ## Environment 8 | 9 | Import [CommunityPatch.Dev.postman_environment.json](CommunityPatch.Dev.postman_environment.json) to setup the environment variables for the collection. 10 | 11 | - **Access Token:** You will need to manually paste your generated access token from CommunityPatch into the _**Current Value**_ field. The access token is used for creating and invalidating API tokens with the Contributors API. 12 | - **Contributor ID:** Your unique identifier. 13 | - **API Token:** When you use the "Create API Token" request it will populate this environment variable for you and be referenced for all other Titles API requests. 14 | - **API Token ID:** This will also be automatically populated for you. If you wish to invalidate the API token that was created using Postman it will reference this environment variable for the "Invalidate API Token" request. 15 | 16 | ## Collection 17 | 18 | Import [CommunityPatch.Dev.postman_collection.json](CommunityPatch.Dev.postman_collection.json) to add the CommunityPatch.Dev API collection to Postman. Every type of requests for each API can be selected by expanding the appropriate folder. 19 | 20 | Certain requests will have parameters in the URLs you will need to provide. You will see their markers with semi-colons: `:title_id` or `:contributor_id`. Click on the **Params** tab in the request to find **Path Variables** that are empty. 21 | 22 | Some requests have been pre-populated with sample data. The "Create Title" and "Add Version" requests have starter JSON payloads for an Xcode patch definition. 23 | -------------------------------------------------------------------------------- /resources/global/cognito.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | 5 | DomainName: 6 | Type: String 7 | 8 | RegionalCertificateArn: 9 | Type: AWS::SSM::Parameter::Value<String> 10 | 11 | Resources: 12 | 13 | # Cognito User Pool 14 | 15 | UserPool: 16 | Type: AWS::Cognito::UserPool 17 | Properties: 18 | UserPoolName: !Ref AWS::StackName 19 | 20 | UserPoolDomain: 21 | Type: AWS::Cognito::UserPoolDomain 22 | Properties: 23 | CustomDomainConfig: 24 | CertificateArn: !Ref RegionalCertificateArn 25 | Domain: !Sub "auth.${DomainName}" 26 | UserPoolId: !Ref UserPool 27 | 28 | # Cognito Resource Servers 29 | 30 | ContributorsApi: 31 | Type: AWS::Cognito::UserPoolResourceServer 32 | Properties: 33 | Identifier: contributors-api 34 | Name: Contributors API 35 | UserPoolId: !Ref UserPool 36 | Scopes: 37 | - ScopeName: full_access 38 | ScopeDescription: Default scope 39 | 40 | TitlesApi: 41 | Type: AWS::Cognito::UserPoolResourceServer 42 | Properties: 43 | Identifier: titles-api 44 | Name: Titles API 45 | UserPoolId: !Ref UserPool 46 | Scopes: 47 | - ScopeName: full_access 48 | ScopeDescription: Default scope 49 | 50 | # Cognito Clients 51 | 52 | AppleIdLoginClient: 53 | Type: AWS::Cognito::UserPoolClient 54 | Properties: 55 | ClientName: appleid-logins 56 | RefreshTokenValidity: 30 57 | UserPoolId: !Ref UserPool 58 | CallbackURLs: 59 | - !Sub "https://contributors.${DomainName}/oauth2/appleid/token" 60 | - !Sub "https://auth.${DomainName}/oauth2/idpresponse" 61 | - !Sub "https://${DomainName}" 62 | LogoutURLs: 63 | - !Sub "https://contributors.${DomainName}/oauth2/appleid/logout" 64 | AllowedOAuthFlowsUserPoolClient: True 65 | AllowedOAuthFlows: 66 | - code 67 | - implicit 68 | AllowedOAuthScopes: 69 | - openid 70 | - contributors-api/full_access 71 | 72 | # Auth Code Sign In URL: https://auth.{DOMAIN}/login?response_type=code&client_id={CLIENT_ID}&redirect_uri=https://contributors.${DOMAIN}/oauth2/appleid/token 73 | # Implicit Sign In URL: https://auth.{DOMAIN}/login?response_type=token&client_id={CLIENT_ID}&redirect_uri=https://{DOMAIN} 74 | 75 | # Stack Outputs 76 | 77 | Outputs: 78 | 79 | CognitoUserPoolArn: 80 | Value: !GetAtt UserPool.Arn 81 | 82 | AppleIdLoginClientId: 83 | Value: !Ref AppleIdLoginClient 84 | -------------------------------------------------------------------------------- /resources/global/tables.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Resources: 4 | 5 | # Global DynamoDB Table 6 | 7 | # Must configure global table in the console at this time. 8 | 9 | CommunityPatchTable: 10 | Type: AWS::DynamoDB::Table 11 | Properties: 12 | BillingMode: PAY_PER_REQUEST 13 | 14 | AttributeDefinitions: 15 | - AttributeName: contributor_id 16 | AttributeType: S 17 | - AttributeName: type 18 | AttributeType: S 19 | - AttributeName: search_index 20 | AttributeType: S 21 | - AttributeName: title_id 22 | AttributeType: S 23 | - AttributeName: alias 24 | AttributeType: S 25 | 26 | KeySchema: 27 | - AttributeName: contributor_id 28 | KeyType: HASH 29 | - AttributeName: type 30 | KeyType: RANGE 31 | 32 | GlobalSecondaryIndexes: 33 | - IndexName: ContributorSummaries 34 | KeySchema: 35 | - AttributeName: contributor_id 36 | KeyType: HASH 37 | - AttributeName: title_id 38 | KeyType: RANGE 39 | Projection: 40 | ProjectionType: INCLUDE 41 | NonKeyAttributes: 42 | - summary 43 | 44 | - IndexName: TitleSearch 45 | KeySchema: 46 | - AttributeName: search_index 47 | KeyType: HASH 48 | - AttributeName: title_id 49 | KeyType: RANGE 50 | Projection: 51 | ProjectionType: INCLUDE 52 | NonKeyAttributes: 53 | - contributor_id 54 | - summary 55 | 56 | - IndexName: ContributorAliasLookup 57 | KeySchema: 58 | - AttributeName: type 59 | KeyType: HASH 60 | - AttributeName: alias 61 | KeyType: RANGE 62 | Projection: 63 | ProjectionType: INCLUDE 64 | NonKeyAttributes: 65 | - contributor_id 66 | 67 | StreamSpecification: 68 | StreamViewType: NEW_AND_OLD_IMAGES 69 | 70 | TimeToLiveSpecification: 71 | AttributeName: ttl 72 | Enabled: true 73 | 74 | # Stack Outputs 75 | 76 | Outputs: 77 | 78 | CommunityPatchTableName: 79 | Value: !Ref CommunityPatchTable 80 | -------------------------------------------------------------------------------- /resources/regional/src/dynamodb_stream_arn_lookup/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import boto3 4 | import requests 5 | 6 | client = boto3.client("dynamodb") 7 | 8 | 9 | def lambda_handler(event, context): 10 | stream_arn = "" 11 | 12 | if event["RequestType"] != "Delete": 13 | try: 14 | table_name = event["ResourceProperties"]["TableName"] 15 | response = client.describe_table(TableName=table_name) 16 | stream_arn = response["Table"]["LatestStreamArn"] 17 | except Exception as error: 18 | cfnresponse( 19 | event, 20 | context, 21 | "FAILED", 22 | {"Error": type(error).__name__, "Message": str(error)}, 23 | ) 24 | 25 | cfnresponse(event, context, "SUCCESS", {"Arn": stream_arn}) 26 | 27 | 28 | def cfnresponse( 29 | event, 30 | context, 31 | response_status, 32 | response_data, 33 | physical_resource_id=None, 34 | no_echo=False, 35 | ): 36 | request_body = json.dumps( 37 | { 38 | "Status": response_status, 39 | "Reason": f"See the details in CloudWatch Log Stream: {context.log_stream_name}", 40 | "PhysicalResourceId": physical_resource_id or context.log_stream_name, 41 | "StackId": event["StackId"], 42 | "RequestId": event["RequestId"], 43 | "LogicalResourceId": event["LogicalResourceId"], 44 | "NoEcho": no_echo, 45 | "Data": response_data, 46 | } 47 | ) 48 | 49 | print(f"Request URL:\n{event['ResponseURL']}") 50 | print(f"Request body:\n{request_body}") 51 | 52 | try: 53 | response = requests.put( 54 | event["ResponseURL"], 55 | data=request_body, 56 | headers={"content-type": "", "content-length": str(len(request_body))}, 57 | ) 58 | print(f"Request result: {response.status_code} {response.reason}") 59 | response.raise_for_status() 60 | except Exception as e: 61 | print("send(..) failed executing requests.put(..): " + str(e)) 62 | -------------------------------------------------------------------------------- /resources/regional/src/dynamodb_stream_arn_lookup/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /resources/regional/src/stream_processor/index.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import json 3 | import os 4 | 5 | import boto3 6 | 7 | EVENT_BUS = f"{os.getenv('NAMESPACE')}-communitypatch" 8 | 9 | events_client = boto3.client("events") 10 | 11 | 12 | def lambda_handler(event, context): 13 | events_to_put = [] 14 | 15 | for record in event["Records"]: 16 | print(f"Event: {record['eventName']}/{record['eventID']}") 17 | table_arn, _ = record["eventSourceARN"].split("/stream") 18 | events_to_put.append( 19 | { 20 | "Time": datetime.utcnow(), 21 | "Source": "communitypatch.table", 22 | "Resources": [table_arn], 23 | "DetailType": "Table Change", 24 | "Detail": json.dumps(record), 25 | "EventBusName": EVENT_BUS, 26 | } 27 | ) 28 | 29 | if events_to_put: 30 | print(f"Publishing {len(events_to_put)} events to {EVENT_BUS}") 31 | events_client.put_events(Entries=events_to_put) 32 | 33 | return "ok" 34 | -------------------------------------------------------------------------------- /resources/regional/src/stream_processor/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/resources/regional/src/stream_processor/requirements.txt -------------------------------------------------------------------------------- /resources/regional/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Regional setup and pre-requisites for a Community Patch region 4 | 5 | Parameters: 6 | 7 | Namespace: 8 | Type: String 9 | 10 | DomainName: 11 | Type: String 12 | 13 | HostedZoneId: 14 | Type: String 15 | 16 | CommunityPatchTableName: 17 | Type: String 18 | 19 | # SAM Globals 20 | 21 | Globals: 22 | Function: 23 | Runtime: python3.7 24 | Handler: index.lambda_handler 25 | Tracing: Active 26 | Environment: 27 | Variables: 28 | NAMESPACE: !Ref Namespace 29 | 30 | Resources: 31 | 32 | # Table Stream ARN Lookup 33 | 34 | DynamoDBStreamArnLookup: 35 | Type: AWS::Serverless::Function 36 | Properties: 37 | CodeUri: ./src/dynamodb_stream_arn_lookup 38 | Policies: 39 | - Statement: 40 | - Effect: Allow 41 | Action: dynamodb:Describe* 42 | Resource: '*' 43 | 44 | CommunityPatchTableStream: 45 | Type: AWS::CloudFormation::CustomResource 46 | Properties: 47 | ServiceToken: !GetAtt DynamoDBStreamArnLookup.Arn 48 | TableName: !Ref CommunityPatchTableName 49 | 50 | # EventBridge 51 | 52 | DataEvents: 53 | Type: AWS::Events::EventBus 54 | Properties: 55 | Name: !Sub ${Namespace}-communitypatch 56 | 57 | TableEvents: 58 | Type: AWS::Serverless::Function 59 | Properties: 60 | CodeUri: ./src/stream_processor 61 | Policies: 62 | - Statement: 63 | - Effect: Allow 64 | Action: events:PutEvents 65 | Resource: !GetAtt DataEvents.Arn 66 | Events: 67 | DeploymentsTableEvent: 68 | Type: DynamoDB 69 | Properties: 70 | Stream: !GetAtt CommunityPatchTableStream.Arn 71 | StartingPosition: TRIM_HORIZON 72 | BatchSize: 10 73 | 74 | -------------------------------------------------------------------------------- /src/layers/api_shared/api_helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def response(message, status_code): 5 | """Returns a dictionary object for an API Gateway Lambda integration 6 | response. 7 | 8 | :param message: Message for JSON body of response 9 | :type message: str or dict 10 | 11 | :param int status_code: HTTP status code of response 12 | 13 | :rtype: dict 14 | """ 15 | if isinstance(message, str): 16 | message = {'message': message} 17 | 18 | return { 19 | 'isBase64Encoded': False, 20 | 'statusCode': status_code, 21 | 'body': json.dumps(message), 22 | 'headers': {'Content-Type': 'application/json'} 23 | } 24 | -------------------------------------------------------------------------------- /src/layers/api_shared/requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema==3.0.2 2 | git+https://github.com/brysontyrrell/Opossum.git#egg=opossum 3 | -------------------------------------------------------------------------------- /src/layers/security_shared/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography==2.7 2 | jsonschema==3.0.2 3 | pyjwt==1.7.1 4 | -------------------------------------------------------------------------------- /src/layers/security_shared/security_helpers.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import time 4 | import uuid 5 | 6 | import boto3 7 | from cryptography.fernet import Fernet 8 | import jwt 9 | 10 | PARAM_STORE_PATH = os.getenv('PARAM_STORE_PATH') 11 | 12 | ssm_client = boto3.client('ssm') 13 | 14 | 15 | def get_parameters(param_names): 16 | return_params = dict() 17 | 18 | resp = ssm_client.get_parameters( 19 | Names=[os.path.join(PARAM_STORE_PATH, i) for i in param_names], 20 | WithDecryption=True 21 | ) 22 | 23 | for parameter in resp['Parameters']: 24 | name = os.path.basename(parameter['Name']) 25 | 26 | if name.startswith('token'): 27 | value = base64.b64decode(parameter['Value']) 28 | else: 29 | value = parameter['Value'] 30 | 31 | return_params[name] = value 32 | 33 | return return_params 34 | 35 | 36 | parameters = get_parameters( 37 | ('database_key', 'legacy_api_key', 'token_private_key', 'token_public_key') 38 | ) 39 | 40 | 41 | def get_fernet(): 42 | return Fernet(parameters['database_key']) 43 | 44 | 45 | def create_token(contributor_id): 46 | token_id = uuid.uuid4().hex 47 | now = int(time.time()) 48 | 49 | api_token = jwt.encode( 50 | { 51 | 'jti': token_id, 52 | 'sub': contributor_id, 53 | 'iat': now, 54 | 'exp': now + 31536000 # one year 55 | }, 56 | parameters['token_private_key'], 57 | algorithm='RS256' 58 | ).decode() 59 | return api_token, token_id 60 | 61 | 62 | def create_legacy_token(contributor_id): 63 | token_id = uuid.uuid4().hex 64 | api_token = jwt.encode( 65 | { 66 | 'jti': token_id, 67 | 'sub': contributor_id 68 | }, 69 | parameters['legacy_api_key'], 70 | algorithm='HS256' 71 | ).decode() 72 | return api_token, token_id 73 | 74 | 75 | def validate_token(token): 76 | headers = jwt.get_unverified_header(token) 77 | 78 | if headers['alg'] == 'HS256': 79 | algorithm = 'HS256' 80 | signing_secret = parameters['legacy_api_key'] 81 | 82 | elif headers['alg'] == 'RS256': 83 | algorithm = 'RS256' 84 | signing_secret = parameters['token_public_key'] 85 | 86 | else: 87 | raise Exception('Unauthorized') 88 | 89 | try: 90 | decoded_token = jwt.decode(token, signing_secret, algorithms=algorithm) 91 | except jwt.InvalidTokenError: 92 | raise Exception('Unauthorized') 93 | 94 | return decoded_token 95 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: A community managed patch server for Jamf Pro. 4 | 5 | Parameters: 6 | 7 | ParameterStorePath: 8 | Type: String 9 | Description: The root path for parameter store values. 10 | Default: /communitypatch 11 | 12 | DomainName: 13 | Type: String 14 | Description: The custom domain name for the API. 15 | 16 | HostedZoneId: 17 | Type: AWS::Route53::HostedZone::Id 18 | Description: The ID of hosted zone for the Route53 record. 19 | 20 | CertificateId: 21 | Type: String 22 | Description: The UUID for the certificate to use for the custom domain. 23 | 24 | # SAM Globals 25 | 26 | Globals: 27 | Function: 28 | Runtime: python3.7 29 | Handler: index.lambda_handler 30 | MemorySize: 512 31 | Environment: 32 | Variables: 33 | CONTRIBUTORS_TABLE: !Ref ContributorsTable 34 | DOMAIN_NAME: !Ref DomainName 35 | EMAIL_SNS_TOPIC: !Ref SendEmailTopic 36 | PARAM_STORE_PATH: !Ref ParameterStorePath 37 | TITLES_BUCKET: !Ref TitlesBucket 38 | TITLES_TABLE: !Ref TitlesTable 39 | 40 | Resources: 41 | 42 | # S3 Buckets 43 | 44 | TitlesBucket: 45 | Type: AWS::S3::Bucket 46 | 47 | TitlesBucketPolicy: 48 | Type: AWS::S3::BucketPolicy 49 | Properties: 50 | Bucket: !Ref TitlesBucket 51 | PolicyDocument: 52 | Statement: 53 | - Action: s3:GetObject 54 | Effect: Allow 55 | Resource: !Sub '${TitlesBucket.Arn}/*' 56 | Principal: 57 | AWS: !Sub 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}' 58 | 59 | WebContentBucket: 60 | Type: AWS::S3::Bucket 61 | 62 | WebContentBucketPolicy: 63 | Type: AWS::S3::BucketPolicy 64 | Properties: 65 | Bucket: !Ref WebContentBucket 66 | PolicyDocument: 67 | Statement: 68 | - Action: s3:GetObject 69 | Effect: Allow 70 | Resource: !Sub '${WebContentBucket.Arn}/*' 71 | Principal: 72 | AWS: !Sub 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}' 73 | 74 | # DynamoDB Tables 75 | 76 | ContributorsTable: 77 | Type: AWS::Serverless::SimpleTable 78 | Properties: 79 | PrimaryKey: 80 | Name: id 81 | Type: String 82 | 83 | BlackListedTokensTable: 84 | Type: AWS::Serverless::SimpleTable 85 | Properties: 86 | PrimaryKey: 87 | Name: token_id 88 | Type: String 89 | 90 | TitlesTable: 91 | Type: AWS::DynamoDB::Table 92 | Properties: 93 | BillingMode: PAY_PER_REQUEST 94 | 95 | AttributeDefinitions: 96 | - AttributeName: contributor_id 97 | AttributeType: S 98 | - AttributeName: title_id 99 | AttributeType: S 100 | 101 | KeySchema: 102 | - AttributeName: contributor_id 103 | KeyType: HASH 104 | - AttributeName: title_id 105 | KeyType: RANGE 106 | 107 | # SNS Topics 108 | 109 | SendEmailTopic: 110 | Type: AWS::SNS::Topic 111 | 112 | # API Gateway 113 | 114 | ApiGateway: 115 | Type: AWS::Serverless::Api 116 | Properties: 117 | StageName: Prod 118 | DefinitionBody: 119 | swagger: "2.0" 120 | info: 121 | title: !Ref AWS::StackName 122 | 123 | securityDefinitions: 124 | authorizer: 125 | type: apiKey 126 | name: Authorization 127 | in: header 128 | x-amazon-apigateway-authtype: custom 129 | x-amazon-apigateway-authorizer: 130 | type: token 131 | authorizerUri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Authorizer.Arn}/invocations' 132 | authorizerResultTtlInSeconds: 300 133 | 134 | paths: 135 | 136 | "/": 137 | get: 138 | x-amazon-apigateway-integration: 139 | contentHandling: "CONVERT_TO_TEXT" 140 | type: aws 141 | httpMethod: post 142 | uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${WebView.Arn}/invocations" 143 | passthroughBehavior: "when_no_match" 144 | responses: 145 | default: 146 | statusCode: "200" 147 | responseParameters: 148 | method.response.header.Content-Type: "'text/html'" 149 | responseTemplates: 150 | text/html: "$input.path('$')" 151 | produces: 152 | - "text/html" 153 | responses: 154 | "200": 155 | description: "200 response" 156 | headers: 157 | Content-Type: 158 | type: "string" 159 | 160 | "/api/v1/contributors": 161 | get: 162 | x-amazon-apigateway-integration: 163 | httpMethod: post 164 | type: aws_proxy 165 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiContributorsGet.Arn}/invocations' 166 | responses: {} 167 | 168 | "/api/v1/contributors/register": 169 | post: 170 | x-amazon-apigateway-integration: 171 | httpMethod: post 172 | type: aws_proxy 173 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiContributorRegistration.Arn}/invocations' 174 | responses: {} 175 | 176 | "/api/v1/contributors/verify": 177 | get: 178 | x-amazon-apigateway-integration: 179 | httpMethod: post 180 | type: aws_proxy 181 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiContributorVerification.Arn}/invocations' 182 | responses: {} 183 | 184 | "/api/v1/titles": 185 | post: 186 | x-amazon-apigateway-integration: 187 | httpMethod: post 188 | type: aws_proxy 189 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiTitlesCreate.Arn}/invocations' 190 | responses: {} 191 | security: 192 | - authorizer: [] 193 | 194 | "/api/v1/titles/{title}/version": 195 | post: 196 | x-amazon-apigateway-integration: 197 | httpMethod: post 198 | type: aws_proxy 199 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiTitleVersions.Arn}/invocations' 200 | responses: {} 201 | security: 202 | - authorizer: [] 203 | 204 | "/api/v1/titles/{title}/version/{version}": 205 | delete: 206 | x-amazon-apigateway-integration: 207 | httpMethod: post 208 | type: aws_proxy 209 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiTitleVersions.Arn}/invocations' 210 | responses: {} 211 | security: 212 | - authorizer: [] 213 | 214 | "/api/v1/titles/{title}/versions": 215 | post: 216 | x-amazon-apigateway-integration: 217 | httpMethod: post 218 | type: aws_proxy 219 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiTitleVersions.Arn}/invocations' 220 | responses: {} 221 | security: 222 | - authorizer: [] 223 | 224 | "/api/v1/titles/{title}/versions/{version}": 225 | delete: 226 | x-amazon-apigateway-integration: 227 | httpMethod: post 228 | type: aws_proxy 229 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiTitleVersions.Arn}/invocations' 230 | responses: {} 231 | security: 232 | - authorizer: [] 233 | 234 | "/api/v1/titles/{title}": 235 | delete: 236 | x-amazon-apigateway-integration: 237 | httpMethod: post 238 | type: aws_proxy 239 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiTitlesDelete.Arn}/invocations' 240 | responses: {} 241 | security: 242 | - authorizer: [] 243 | 244 | "/jamf/v1/{contributor}/software": 245 | get: 246 | x-amazon-apigateway-integration: 247 | httpMethod: post 248 | type: aws_proxy 249 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiJamfSoftware.Arn}/invocations' 250 | responses: {} 251 | 252 | "/jamf/v1/{contributor}/software/{titles}": 253 | get: 254 | x-amazon-apigateway-integration: 255 | httpMethod: post 256 | type: aws_proxy 257 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiJamfSoftware.Arn}/invocations' 258 | responses: {} 259 | 260 | "/jamf/v1/{contributor}/patch/{title}": 261 | get: 262 | x-amazon-apigateway-integration: 263 | httpMethod: post 264 | type: aws_proxy 265 | uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiJamfPatch.Arn}/invocations' 266 | responses: {} 267 | 268 | # CloudFront Distribution 269 | 270 | CloudFrontOriginAccessIdentity: 271 | Type: AWS::CloudFront::CloudFrontOriginAccessIdentity 272 | Properties: 273 | CloudFrontOriginAccessIdentityConfig: 274 | Comment: !Sub '${AWS::StackName}-origin-access-identity' 275 | 276 | CloudFrontDistribution: 277 | Type: AWS::CloudFront::Distribution 278 | Properties: 279 | DistributionConfig: 280 | Enabled: true 281 | Comment: Unified domain for CommunityPatch. 282 | 283 | Aliases: 284 | - !Ref DomainName 285 | 286 | ViewerCertificate: 287 | AcmCertificateArn: !Sub 'arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CertificateId}' 288 | SslSupportMethod: sni-only 289 | MinimumProtocolVersion: TLSv1.1_2016 290 | 291 | DefaultCacheBehavior: 292 | AllowedMethods: 293 | - DELETE 294 | - GET 295 | - HEAD 296 | - OPTIONS 297 | - PATCH 298 | - POST 299 | - PUT 300 | CachedMethods: 301 | - GET 302 | - HEAD 303 | - OPTIONS 304 | Compress: true 305 | DefaultTTL: 300 306 | ForwardedValues: 307 | Headers: 308 | - Accept 309 | - Referer 310 | - Authorization 311 | - Content-Type 312 | QueryString: true 313 | MaxTTL: 300 314 | TargetOriginId: ApiGatewayOrigin 315 | ViewerProtocolPolicy: https-only 316 | 317 | CacheBehaviors: 318 | - Compress: true 319 | DefaultTTL: 300 320 | ForwardedValues: 321 | QueryString: true 322 | PathPattern: '/titles/*' 323 | MaxTTL: 300 324 | TargetOriginId: TitlesBucketOrigin 325 | ViewerProtocolPolicy: allow-all 326 | 327 | - Compress: true 328 | ForwardedValues: 329 | QueryString: true 330 | PathPattern: '/images/*' 331 | TargetOriginId: WebContentBucketOrigin 332 | ViewerProtocolPolicy: allow-all 333 | 334 | - Compress: true 335 | ForwardedValues: 336 | QueryString: true 337 | PathPattern: '/css/*' 338 | TargetOriginId: WebContentBucketOrigin 339 | ViewerProtocolPolicy: allow-all 340 | 341 | Origins: 342 | - DomainName: !Sub '${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com' 343 | Id: ApiGatewayOrigin 344 | CustomOriginConfig: 345 | OriginProtocolPolicy: https-only 346 | OriginPath: /Prod 347 | 348 | - DomainName: !GetAtt TitlesBucket.DomainName 349 | Id: TitlesBucketOrigin 350 | S3OriginConfig: 351 | OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}' 352 | 353 | - DomainName: !GetAtt WebContentBucket.DomainName 354 | Id: WebContentBucketOrigin 355 | S3OriginConfig: 356 | OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}' 357 | 358 | # Route 53 Record 359 | 360 | CloudFrontRoute53Record: 361 | Type: AWS::Route53::RecordSet 362 | Properties: 363 | Name: !Ref DomainName 364 | Type: A 365 | HostedZoneId: !Ref HostedZoneId 366 | AliasTarget: 367 | DNSName: !GetAtt CloudFrontDistribution.DomainName 368 | EvaluateTargetHealth: false 369 | HostedZoneId: Z2FDTNDATAQYW2 370 | 371 | # Lambda Layers 372 | 373 | ApiShared: 374 | Type: AWS::Serverless::Function 375 | Properties: 376 | CodeUri: ./src/layers/api_shared 377 | 378 | ApiSharedLayer: 379 | Type: AWS::Serverless::LayerVersion 380 | Properties: 381 | ContentUri: ./.aws-sam/build/ApiShared 382 | CompatibleRuntimes: 383 | - python3.6 384 | RetentionPolicy: Delete 385 | DependsOn: 386 | - ApiShared 387 | 388 | SecurityShared: 389 | Type: AWS::Serverless::Function 390 | Properties: 391 | CodeUri: ./src/layers/security_shared 392 | 393 | SecuritySharedLayer: 394 | Type: AWS::Serverless::LayerVersion 395 | Properties: 396 | ContentUri: ./.aws-sam/build/SecurityShared 397 | CompatibleRuntimes: 398 | - python3.6 399 | RetentionPolicy: Delete 400 | DependsOn: 401 | - SecurityShared 402 | 403 | # Web View 404 | 405 | WebView: 406 | Type: AWS::Serverless::Function 407 | Description: Renders the main web page displaying all available titles. 408 | Properties: 409 | CodeUri: ./src/functions/web_view 410 | Timeout: 15 411 | Events: 412 | Index: 413 | Type: Api 414 | Properties: 415 | Path: / 416 | Method: get 417 | RestApiId: 418 | Ref: ApiGateway 419 | 420 | 421 | # Contributor Management 422 | 423 | ApiContributorRegistration: 424 | Type: AWS::Serverless::Function 425 | Description: Creates and rotates tokens. 426 | Properties: 427 | CodeUri: ./src/functions/contributors/api_contributor_registration 428 | Handler: api_contributor_registration.lambda_handler 429 | Layers: 430 | - !Ref ApiSharedLayer 431 | - !Ref SecuritySharedLayer 432 | Policies: 433 | Statement: 434 | - Effect: Allow 435 | Action: dynamodb:PutItem 436 | Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ContributorsTable}' 437 | - Effect: Allow 438 | Action: sns:Publish 439 | Resource: !Ref SendEmailTopic 440 | - Effect: Allow 441 | Action: ssm:GetParameter* 442 | Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter${ParameterStorePath}*' 443 | Events: 444 | ApiContributorRegistration: 445 | Type: Api 446 | Properties: 447 | Path: /api/v1/contributors/register 448 | Method: post 449 | RestApiId: 450 | Ref: ApiGateway 451 | 452 | ApiContributorVerification: 453 | Type: AWS::Serverless::Function 454 | Description: Generate an API token upon account verification. 455 | Properties: 456 | CodeUri: ./src/functions/contributors/api_contributor_verification 457 | Handler: api_contributor_verification.lambda_handler 458 | Layers: 459 | - !Ref SecuritySharedLayer 460 | Policies: 461 | Statement: 462 | - Effect: Allow 463 | Action: 464 | - dynamodb:GetItem 465 | - dynamodb:UpdateItem 466 | Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ContributorsTable}' 467 | - Effect: Allow 468 | Action: sns:Publish 469 | Resource: !Ref SendEmailTopic 470 | - Effect: Allow 471 | Action: ssm:GetParameter* 472 | Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter${ParameterStorePath}*' 473 | Events: 474 | ApiContributorRegistration: 475 | Type: Api 476 | Properties: 477 | Path: /api/v1/contributors/verify 478 | Method: get 479 | RestApiId: 480 | Ref: ApiGateway 481 | 482 | ApiContributorsGet: 483 | Type: AWS::Serverless::Function 484 | Description: List contributors on CommunityPatch. 485 | Properties: 486 | CodeUri: ./src/functions/contributors/api_contributors_get 487 | Handler: api_contributors_get.lambda_handler 488 | Timeout: 15 489 | Layers: 490 | - !Ref ApiSharedLayer 491 | - !Ref SecuritySharedLayer 492 | Policies: 493 | - DynamoDBReadPolicy: 494 | TableName: !Ref ContributorsTable 495 | - DynamoDBReadPolicy: 496 | TableName: !Ref TitlesTable 497 | Events: 498 | ApiContributorRegistration: 499 | Type: Api 500 | Properties: 501 | Path: /api/v1/contributors 502 | Method: get 503 | RestApiId: 504 | Ref: ApiGateway 505 | 506 | # Token Authorizer 507 | 508 | Authorizer: 509 | Type: AWS::Serverless::Function 510 | Description: Token validation for Uploader. 511 | Properties: 512 | CodeUri: ./src/functions/authorizer 513 | Handler: authorizer.lambda_handler 514 | Layers: 515 | - !Ref SecuritySharedLayer 516 | Environment: 517 | Variables: 518 | BLACKLIST_TABLE: !Ref BlackListedTokensTable 519 | Policies: 520 | Statement: 521 | - Effect: Allow 522 | Action: dynamodb:GetItem 523 | Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${BlackListedTokensTable}' 524 | - Effect: Allow 525 | Action: ssm:GetParameter* 526 | Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter${ParameterStorePath}*' 527 | 528 | AuthorizerPermissions: 529 | Type: AWS::Lambda::Permission 530 | DependsOn: 531 | - ApiGateway 532 | - Authorizer 533 | Properties: 534 | Action: lambda:InvokeFunction 535 | FunctionName: 536 | Ref: Authorizer 537 | Principal: apigateway.amazonaws.com 538 | 539 | # Titles API 540 | 541 | ApiTitlesCreate: 542 | Type: AWS::Serverless::Function 543 | Description: Handles API requests for creating software title definitions. 544 | Properties: 545 | CodeUri: ./src/functions/titles/api_titles_create 546 | Handler: api_titles_create.lambda_handler 547 | Layers: 548 | - !Ref ApiSharedLayer 549 | Policies: 550 | - DynamoDBCrudPolicy: 551 | TableName: !Ref TitlesTable 552 | - S3CrudPolicy: 553 | BucketName: !Ref TitlesBucket 554 | Events: 555 | CreateTitle: 556 | Type: Api 557 | Properties: 558 | Path: /api/v1/titles 559 | Method: post 560 | RestApiId: 561 | Ref: ApiGateway 562 | 563 | ApiTitleVersions: 564 | Type: AWS::Serverless::Function 565 | Properties: 566 | CodeUri: ./src/functions/titles/api_versions 567 | Layers: 568 | - !Ref ApiSharedLayer 569 | Policies: 570 | - DynamoDBCrudPolicy: 571 | TableName: !Ref TitlesTable 572 | - S3CrudPolicy: 573 | BucketName: !Ref TitlesBucket 574 | Events: 575 | NewVersionDep: 576 | Type: Api 577 | Properties: 578 | Path: /api/v1/titles/{title}/version 579 | Method: post 580 | RestApiId: 581 | Ref: ApiGateway 582 | DeleteVersionDep: 583 | Type: Api 584 | Properties: 585 | Path: /api/v1/titles/{title}/version/{version} 586 | Method: delete 587 | RestApiId: 588 | Ref: ApiGateway 589 | NewVersion: 590 | Type: Api 591 | Properties: 592 | Path: /api/v1/titles/{title}/versions 593 | Method: post 594 | RestApiId: 595 | Ref: ApiGateway 596 | DeleteVersion: 597 | Type: Api 598 | Properties: 599 | Path: /api/v1/titles/{title}/versions/{version} 600 | Method: delete 601 | RestApiId: 602 | Ref: ApiGateway 603 | 604 | ApiTitlesDelete: 605 | Type: AWS::Serverless::Function 606 | Description: Handles API requests for deleting patch definitions. 607 | Properties: 608 | CodeUri: ./src/functions/titles/api_titles_delete 609 | Handler: api_titles_delete.lambda_handler 610 | Layers: 611 | - !Ref ApiSharedLayer 612 | Policies: 613 | Statement: 614 | - Effect: Allow 615 | Action: s3:DeleteObject 616 | Resource: !Sub 'arn:aws:s3:::${TitlesBucket}/*' 617 | - Effect: Allow 618 | Action: 619 | - dynamodb:GetItem 620 | - dynamodb:DeleteItem 621 | Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TitlesTable}' 622 | Events: 623 | DeleteTitle: 624 | Type: Api 625 | Properties: 626 | Path: /api/v1/titles/{title} 627 | Method: delete 628 | RestApiId: 629 | Ref: ApiGateway 630 | 631 | # Jamf API 632 | 633 | ApiJamfSoftware: 634 | Type: AWS::Serverless::Function 635 | Description: Handles Jamf Pro requests for software lists. 636 | Properties: 637 | CodeUri: ./src/functions/jamf/api_jamf_software 638 | Handler: api_jamf_software.lambda_handler 639 | Layers: 640 | - !Ref ApiSharedLayer 641 | Policies: 642 | Statement: 643 | - Effect: Allow 644 | Action: 645 | - dynamodb:GetItem 646 | - dynamodb:Query 647 | Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TitlesTable}' 648 | Events: 649 | GetAllSoftware: 650 | Type: Api 651 | Properties: 652 | Path: /jamf/v1/{contributor}/software 653 | Method: get 654 | RestApiId: 655 | Ref: ApiGateway 656 | GetSelectSoftware: 657 | Type: Api 658 | Properties: 659 | Path: /jamf/v1/{contributor}/software/{titles} 660 | Method: get 661 | RestApiId: 662 | Ref: ApiGateway 663 | 664 | ApiJamfPatch: 665 | Type: AWS::Serverless::Function 666 | Description: Handles Jamf Pro requests for patch definitions. 667 | Properties: 668 | CodeUri: ./src/functions/jamf/api_jamf_patch 669 | Handler: api_jamf_patch.lambda_handler 670 | Layers: 671 | - !Ref ApiSharedLayer 672 | Policies: 673 | Statement: 674 | - Effect: Allow 675 | Action: s3:GetObject 676 | Resource: !Sub 'arn:aws:s3:::${TitlesBucket}/*' 677 | Events: 678 | GetPatch: 679 | Type: Api 680 | Properties: 681 | Path: /jamf/v1/{contributor}/patch/{title} 682 | Method: get 683 | RestApiId: 684 | Ref: ApiGateway 685 | 686 | # Email Service 687 | 688 | EmailService: 689 | Type: AWS::Serverless::Function 690 | Description: Email notification service. 691 | Properties: 692 | Handler: email_service.lambda_handler 693 | CodeUri: ./src/functions/email_service 694 | Policies: 695 | Statement: 696 | - Effect: Allow 697 | Action: ses:SendEmail 698 | Resource: '*' 699 | Condition: 700 | StringEquals: 701 | ses:FromAddress: !Sub 'noreply@${DomainName}' 702 | Events: 703 | SnsTopic: 704 | Type: SNS 705 | Properties: 706 | Topic: !Ref SendEmailTopic -------------------------------------------------------------------------------- /web/content/css/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/css/custom.css -------------------------------------------------------------------------------- /web/content/css/dataTables.conditionalPaging.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary ConditionalPaging 3 | * @description Hide paging controls when the amount of pages is <= 1 4 | * @version 1.0.0 5 | * @file dataTables.conditionalPaging.js 6 | * @author Matthew Hasbach (https://github.com/mjhasbach) 7 | * @contact hasbach.git@gmail.com 8 | * @copyright Copyright 2015 Matthew Hasbach 9 | * 10 | * License MIT - http://datatables.net/license/mit 11 | * 12 | * This feature plugin for DataTables hides paging controls when the amount 13 | * of pages is <= 1. The controls can either appear / disappear or fade in / out 14 | * 15 | * @example 16 | * $('#myTable').DataTable({ 17 | * conditionalPaging: true 18 | * }); 19 | * 20 | * @example 21 | * $('#myTable').DataTable({ 22 | * conditionalPaging: { 23 | * style: 'fade', 24 | * speed: 500 // optional 25 | * } 26 | * }); 27 | */ 28 | 29 | (function(window, document, $) { 30 | $(document).on('init.dt', function(e, dtSettings) { 31 | if ( e.namespace !== 'dt' ) { 32 | return; 33 | } 34 | 35 | var options = dtSettings.oInit.conditionalPaging || $.fn.dataTable.defaults.conditionalPaging; 36 | 37 | if ($.isPlainObject(options) || options === true) { 38 | var config = $.isPlainObject(options) ? options : {}, 39 | api = new $.fn.dataTable.Api(dtSettings), 40 | speed = 'slow', 41 | conditionalPaging = function(e) { 42 | var $paging = $(api.table().container()).find('div.dataTables_paginate'), 43 | pages = api.page.info().pages; 44 | 45 | if (e instanceof $.Event) { 46 | if (pages <= 1) { 47 | if (config.style === 'fade') { 48 | $paging.stop().fadeTo(speed, 0); 49 | } 50 | else { 51 | $paging.css('visibility', 'hidden'); 52 | } 53 | } 54 | else { 55 | if (config.style === 'fade') { 56 | $paging.stop().fadeTo(speed, 1); 57 | } 58 | else { 59 | $paging.css('visibility', ''); 60 | } 61 | } 62 | } 63 | else if (pages <= 1) { 64 | if (config.style === 'fade') { 65 | $paging.css('opacity', 0); 66 | } 67 | else { 68 | $paging.css('visibility', 'hidden'); 69 | } 70 | } 71 | }; 72 | 73 | if ( config.speed !== undefined ) { 74 | speed = config.speed; 75 | } 76 | 77 | conditionalPaging(); 78 | 79 | api.on('draw.dt', conditionalPaging); 80 | } 81 | }); 82 | })(window, document, jQuery); 83 | -------------------------------------------------------------------------------- /web/content/images/favicon/114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/114x114.png -------------------------------------------------------------------------------- /web/content/images/favicon/120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/120x120.png -------------------------------------------------------------------------------- /web/content/images/favicon/144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/144x144.png -------------------------------------------------------------------------------- /web/content/images/favicon/150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/150x150.png -------------------------------------------------------------------------------- /web/content/images/favicon/152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/152x152.png -------------------------------------------------------------------------------- /web/content/images/favicon/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/16x16.png -------------------------------------------------------------------------------- /web/content/images/favicon/180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/180x180.png -------------------------------------------------------------------------------- /web/content/images/favicon/192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/192x192.png -------------------------------------------------------------------------------- /web/content/images/favicon/310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/310x310.png -------------------------------------------------------------------------------- /web/content/images/favicon/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/32x32.png -------------------------------------------------------------------------------- /web/content/images/favicon/36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/36x36.png -------------------------------------------------------------------------------- /web/content/images/favicon/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/48x48.png -------------------------------------------------------------------------------- /web/content/images/favicon/57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/57x57.png -------------------------------------------------------------------------------- /web/content/images/favicon/60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/60x60.png -------------------------------------------------------------------------------- /web/content/images/favicon/70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/70x70.png -------------------------------------------------------------------------------- /web/content/images/favicon/72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/72x72.png -------------------------------------------------------------------------------- /web/content/images/favicon/76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/76x76.png -------------------------------------------------------------------------------- /web/content/images/favicon/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/96x96.png -------------------------------------------------------------------------------- /web/content/images/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /web/content/images/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/apple-icon.png -------------------------------------------------------------------------------- /web/content/images/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig> -------------------------------------------------------------------------------- /web/content/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/CommunityPatch/e09d8a7704aab8f644f625d52e7d0aac97fba0c2/web/content/images/favicon/favicon.ico -------------------------------------------------------------------------------- /web/content/images/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /web/content/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | 4 | <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> 5 | <script type="text/javascript"> 6 | function urlHash() { 7 | var hash = window.location.hash.substr(1); 8 | var result = hash.split('&').reduce(function (result, item) { 9 | var parts = item.split('='); 10 | result[parts[0]] = parts[1]; 11 | return result; 12 | }, {}); 13 | console.log(result); 14 | document.body.innerHTML = document.body.innerHTML.replace('ID_TOKEN', result.id_token); 15 | document.body.innerHTML = document.body.innerHTML.replace('ACCESS_TOKEN', result.access_token); 16 | document.body.innerHTML = document.body.innerHTML.replace('EXPIRES_IN', result.expires_in); 17 | document.body.innerHTML = document.body.innerHTML.replace('TOKEN_TYPE', result.token_type); 18 | } 19 | </script> 20 | 21 | </head> 22 | 23 | <body onload='urlHash();'> 24 | 25 | <div style="container-fluid; margin: 2rem;"> 26 | <div class="col-md-12 main"> 27 | <b>ID Token:</b> 28 | <br> 29 | <code style='white-space:pre-line;'>ID_TOKEN</code> 30 | </div> 31 | <br> 32 | <div class="col-md-12 main"> 33 | <b>Access Token:</b> 34 | <br> 35 | <code style='white-space:pre-line;'>ACCESS_TOKEN</code> 36 | </div> 37 | <br> 38 | <div class="col-md-12 main"> 39 | <b>Expires In:</b> 40 | <pre>EXPIRES_IN</pre> 41 | </div> 42 | <div class="col-md-12 main"> 43 | <b>Token Type:</b> 44 | <pre>TOKEN_TYPE</pre> 45 | </div> 46 | </div> 47 | 48 | <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> 49 | <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> 50 | <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> 51 | 52 | </body> 53 | 54 | </html> 55 | -------------------------------------------------------------------------------- /web/content/js/custom.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/kmaida/6045266 2 | function ConvertTimestamp(timestamp) { 3 | var d = new Date(timestamp * 1000), // Convert the passed timestamp to milliseconds 4 | yyyy = d.getFullYear(), 5 | mm = ('0' + (d.getMonth() + 1)).slice(-2), // Months are zero based. Add leading 0. 6 | dd = ('0' + d.getDate()).slice(-2), // Add leading 0. 7 | hh = d.getHours(), 8 | h = hh, 9 | min = ('0' + d.getMinutes()).slice(-2), // Add leading 0. 10 | ampm = 'AM', 11 | time; 12 | 13 | if (hh > 12) { 14 | h = hh - 12; 15 | ampm = 'PM'; 16 | } else if (hh === 12) { 17 | h = 12; 18 | ampm = 'PM'; 19 | } else if (hh === 0) { 20 | h = 12; 21 | } 22 | 23 | // ie: 2013-02-18, 8:35 AM 24 | time = yyyy + '-' + mm + '-' + dd + ' ' + h + ':' + min + ' ' + ampm; 25 | 26 | return time; 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /web/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | 5 | DomainName: 6 | Type: String 7 | 8 | HostedZoneId: 9 | Type: String 10 | 11 | RegionalCertificateArn: 12 | Type: AWS::SSM::Parameter::Value<String> 13 | 14 | Resources: 15 | 16 | WebContentBucket: 17 | Type: AWS::S3::Bucket 18 | 19 | WebContentBucketPolicy: 20 | Type: AWS::S3::BucketPolicy 21 | Properties: 22 | Bucket: !Ref WebContentBucket 23 | PolicyDocument: 24 | Statement: 25 | - Action: s3:GetObject 26 | Effect: Allow 27 | Resource: !Sub '${WebContentBucket.Arn}/*' 28 | Principal: 29 | AWS: !Sub 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}' 30 | 31 | CloudFrontOriginAccessIdentity: 32 | Type: AWS::CloudFront::CloudFrontOriginAccessIdentity 33 | Properties: 34 | CloudFrontOriginAccessIdentityConfig: 35 | Comment: !Sub '${AWS::StackName}-origin-access-identity' 36 | 37 | CloudFrontDistribution: 38 | Type: AWS::CloudFront::Distribution 39 | Properties: 40 | DistributionConfig: 41 | Enabled: true 42 | DefaultRootObject: index.html 43 | 44 | Aliases: 45 | - !Ref DomainName 46 | 47 | ViewerCertificate: 48 | AcmCertificateArn: !Ref RegionalCertificateArn 49 | SslSupportMethod: sni-only 50 | MinimumProtocolVersion: TLSv1.1_2016 51 | 52 | DefaultCacheBehavior: 53 | Compress: true 54 | DefaultTTL: 300 55 | ForwardedValues: 56 | QueryString: true 57 | MaxTTL: 300 58 | TargetOriginId: WebContentBucketOrigin 59 | ViewerProtocolPolicy: https-only 60 | 61 | Origins: 62 | - DomainName: !GetAtt WebContentBucket.DomainName 63 | Id: WebContentBucketOrigin 64 | S3OriginConfig: 65 | OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}' 66 | 67 | # Route 53 Record 68 | 69 | CloudFrontRoute53Record: 70 | Type: AWS::Route53::RecordSet 71 | Properties: 72 | Name: !Ref DomainName 73 | Type: A 74 | HostedZoneId: !Ref HostedZoneId 75 | AliasTarget: 76 | DNSName: !GetAtt CloudFrontDistribution.DomainName 77 | EvaluateTargetHealth: false 78 | HostedZoneId: Z2FDTNDATAQYW2 79 | --------------------------------------------------------------------------------