From 03468d1450dccf84e493a1dc022b866291a2cc91 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 30 Jul 2024 16:13:45 +0200 Subject: [PATCH 1/5] Add docs about token permissions (#1829) * add docs on token perms * bump version * Update with Contents:read * nitpick change --- charm/charmcraft.yaml | 9 ++++----- charm/docs/reference/token-permissions.md | 20 ++++++++++++++++++++ pyproject.toml | 2 +- rockcraft.yaml | 2 +- 4 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 charm/docs/reference/token-permissions.md diff --git a/charm/charmcraft.yaml b/charm/charmcraft.yaml index 98ab3a65..608f46df 100644 --- a/charm/charmcraft.yaml +++ b/charm/charmcraft.yaml @@ -50,8 +50,7 @@ config: default: false github_token: description: >- - The token to use for comms with GitHub. This can be a PAT or a fine-grained token - with permissions to read collaborators (and collaborators' permissions) and branches - for all repositories that need to be checked. - type: string - required: true + The token to use for communication with GitHub. This can be a PAT (with repo scope) + or a fine-grained token with read permission for Administration. If private repositories + are checked, the fine-grained token does also need read permission for Contents and + Pull request. diff --git a/charm/docs/reference/token-permissions.md b/charm/docs/reference/token-permissions.md new file mode 100644 index 00000000..239d6eb4 --- /dev/null +++ b/charm/docs/reference/token-permissions.md @@ -0,0 +1,20 @@ +# GitHub Token Permissions + +You can either choose to use a personal access token (PAT) or a fine-grained access token for the +`github_token` configuration. The token permissions/scopes are different for each type of token. + + +## Fine grained access token permissions + +**Note**: In addition to having a token with the necessary permissions, the user who owns the +token also must have admin access to the organisation or repository. + +For fine-grained access control, the following repository permissions are required: + +- Administration: read +- Contents: read (if you want to check private repositories) +- Pull requests: read (if you want to check private repositories) + +## Personal access token scopes + +If you want to use classic PATS, you will need to select the `repo` scope. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 716091cf..78b21a12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.poetry] name = "repo-policy-compliance" -version = "1.9.1" +version = "1.9.2" description = "Checks GitHub repository settings for compliance with policy" authors = ["Canonical IS DevOps "] license = "Apache 2.0" diff --git a/rockcraft.yaml b/rockcraft.yaml index 1971aea1..d7790164 100644 --- a/rockcraft.yaml +++ b/rockcraft.yaml @@ -3,7 +3,7 @@ name: repo-policy-compliance base: ubuntu@22.04 -version: '1.9.1' +version: '1.9.2' summary: Check the repository setup for policy compliance description: | Used to check whether a GitHub repository complies with expected policies. From 03a3fa3a386e12ed3d0eb47eba19ed35ec4777f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:57:33 +0200 Subject: [PATCH 2/5] chore(deps): update dependency paas-app-charmer to v1.0.4 (#1828) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Amanda H. L. de Andrade Katz --- charm/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charm/requirements.txt b/charm/requirements.txt index be32e2f2..c73cf0c6 100644 --- a/charm/requirements.txt +++ b/charm/requirements.txt @@ -1 +1 @@ -paas-app-charmer==1.0.3 +paas-app-charmer==1.0.4 From f5fee6604ebf1990461bffa7b7f75c7ee8dd56c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:25:14 +0200 Subject: [PATCH 3/5] chore(deps): update dependency rpds-py to v0.19.1 (#1827) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Christopher Bartz --- requirements.txt | 204 ++++++++++++++++++++++++----------------------- 1 file changed, 104 insertions(+), 100 deletions(-) diff --git a/requirements.txt b/requirements.txt index c2ccfdd4..07ef7546 100644 --- a/requirements.txt +++ b/requirements.txt @@ -578,106 +578,110 @@ referencing==0.35.1; python_full_version >= "3.10.0" and python_full_version < " requests==2.32.3; python_full_version >= "3.10.0" and python_full_version < "4.0.0" \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 -rpds-py==0.19.0; python_full_version >= "3.10.0" and python_full_version < "4.0.0" \ - --hash=sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834 \ - --hash=sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4 \ - --hash=sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714 \ - --hash=sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d \ - --hash=sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22 \ - --hash=sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34 \ - --hash=sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff \ - --hash=sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb \ - --hash=sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666 \ - --hash=sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b \ - --hash=sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b \ - --hash=sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e \ - --hash=sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8 \ - --hash=sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b \ - --hash=sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582 \ - --hash=sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34 \ - --hash=sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b \ - --hash=sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581 \ - --hash=sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521 \ - --hash=sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8 \ - --hash=sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc \ - --hash=sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc \ - --hash=sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9 \ - --hash=sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c \ - --hash=sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68 \ - --hash=sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca \ - --hash=sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f \ - --hash=sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb \ - --hash=sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed \ - --hash=sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b \ - --hash=sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600 \ - --hash=sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac \ - --hash=sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5 \ - --hash=sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6 \ - --hash=sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81 \ - --hash=sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9 \ - --hash=sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08 \ - --hash=sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07 \ - --hash=sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec \ - --hash=sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526 \ - --hash=sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766 \ - --hash=sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4 \ - --hash=sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67 \ - --hash=sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c \ - --hash=sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a \ - --hash=sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479 \ - --hash=sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be \ - --hash=sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213 \ - --hash=sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa \ - --hash=sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae \ - --hash=sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf \ - --hash=sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955 \ - --hash=sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952 \ - --hash=sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f \ - --hash=sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210 \ - --hash=sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1 \ - --hash=sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd \ - --hash=sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b \ - --hash=sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c \ - --hash=sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed \ - --hash=sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55 \ - --hash=sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5 \ - --hash=sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b \ - --hash=sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e \ - --hash=sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b \ - --hash=sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a \ - --hash=sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c \ - --hash=sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378 \ - --hash=sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be \ - --hash=sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1 \ - --hash=sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0 \ - --hash=sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a \ - --hash=sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2 \ - --hash=sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633 \ - --hash=sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d \ - --hash=sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223 \ - --hash=sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa \ - --hash=sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533 \ - --hash=sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a \ - --hash=sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc \ - --hash=sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248 \ - --hash=sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05 \ - --hash=sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb \ - --hash=sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d \ - --hash=sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d \ - --hash=sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388 \ - --hash=sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9 \ - --hash=sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d \ - --hash=sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16 \ - --hash=sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2 \ - --hash=sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709 \ - --hash=sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0 \ - --hash=sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336 \ - --hash=sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c \ - --hash=sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179 \ - --hash=sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0 \ - --hash=sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb \ - --hash=sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1 \ - --hash=sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4 +rpds-py==0.19.1; python_full_version >= "3.10.0" and python_full_version < "4.0.0" \ + --hash=sha256:01227f8b3e6c8961490d869aa65c99653df80d2f0a7fde8c64ebddab2b9b02fd \ + --hash=sha256:08ce9c95a0b093b7aec75676b356a27879901488abc27e9d029273d280438505 \ + --hash=sha256:0b02dd77a2de6e49078c8937aadabe933ceac04b41c5dde5eca13a69f3cf144e \ + --hash=sha256:0d4b52811dcbc1aba08fd88d475f75b4f6db0984ba12275d9bed1a04b2cae9b5 \ + --hash=sha256:13e6d4840897d4e4e6b2aa1443e3a8eca92b0402182aafc5f4ca1f5e24f9270a \ + --hash=sha256:1a129c02b42d46758c87faeea21a9f574e1c858b9f358b6dd0bbd71d17713175 \ + --hash=sha256:1a8dfa125b60ec00c7c9baef945bb04abf8ac772d8ebefd79dae2a5f316d7850 \ + --hash=sha256:1c32e41de995f39b6b315d66c27dea3ef7f7c937c06caab4c6a79a5e09e2c415 \ + --hash=sha256:1d494887d40dc4dd0d5a71e9d07324e5c09c4383d93942d391727e7a40ff810b \ + --hash=sha256:1d4af2eb520d759f48f1073ad3caef997d1bfd910dc34e41261a595d3f038a94 \ + --hash=sha256:1fb93d3486f793d54a094e2bfd9cd97031f63fcb5bc18faeb3dd4b49a1c06523 \ + --hash=sha256:24f8ae92c7fae7c28d0fae9b52829235df83f34847aa8160a47eb229d9666c7b \ + --hash=sha256:24fc5a84777cb61692d17988989690d6f34f7f95968ac81398d67c0d0994a897 \ + --hash=sha256:26ab43b6d65d25b1a333c8d1b1c2f8399385ff683a35ab5e274ba7b8bb7dc61c \ + --hash=sha256:271accf41b02687cef26367c775ab220372ee0f4925591c6796e7c148c50cab5 \ + --hash=sha256:2ddd50f18ebc05ec29a0d9271e9dbe93997536da3546677f8ca00b76d477680c \ + --hash=sha256:31dd5794837f00b46f4096aa8ccaa5972f73a938982e32ed817bb520c465e520 \ + --hash=sha256:31e450840f2f27699d014cfc8865cc747184286b26d945bcea6042bb6aa4d26e \ + --hash=sha256:32e0db3d6e4f45601b58e4ac75c6f24afbf99818c647cc2066f3e4b192dabb1f \ + --hash=sha256:346557f5b1d8fd9966059b7a748fd79ac59f5752cd0e9498d6a40e3ac1c1875f \ + --hash=sha256:34bca66e2e3eabc8a19e9afe0d3e77789733c702c7c43cd008e953d5d1463fde \ + --hash=sha256:3511f6baf8438326e351097cecd137eb45c5f019944fe0fd0ae2fea2fd26be39 \ + --hash=sha256:35af5e4d5448fa179fd7fff0bba0fba51f876cd55212f96c8bbcecc5c684ae5c \ + --hash=sha256:3837c63dd6918a24de6c526277910e3766d8c2b1627c500b155f3eecad8fad65 \ + --hash=sha256:39d67896f7235b2c886fb1ee77b1491b77049dcef6fbf0f401e7b4cbed86bbd4 \ + --hash=sha256:3b823be829407393d84ee56dc849dbe3b31b6a326f388e171555b262e8456cc1 \ + --hash=sha256:3c73254c256081704dba0a333457e2fb815364018788f9b501efe7c5e0ada401 \ + --hash=sha256:3ddab996807c6b4227967fe1587febade4e48ac47bb0e2d3e7858bc621b1cace \ + --hash=sha256:3e1dc59a5e7bc7f44bd0c048681f5e05356e479c50be4f2c1a7089103f1621d5 \ + --hash=sha256:4383beb4a29935b8fa28aca8fa84c956bf545cb0c46307b091b8d312a9150e6a \ + --hash=sha256:4cc4bc73e53af8e7a42c8fd7923bbe35babacfa7394ae9240b3430b5dcf16b2a \ + --hash=sha256:4dd02e29c8cbed21a1875330b07246b71121a1c08e29f0ee3db5b4cfe16980c4 \ + --hash=sha256:4f580ae79d0b861dfd912494ab9d477bea535bfb4756a2269130b6607a21802e \ + --hash=sha256:53dbc35808c6faa2ce3e48571f8f74ef70802218554884787b86a30947842a14 \ + --hash=sha256:56313be667a837ff1ea3508cebb1ef6681d418fa2913a0635386cf29cff35165 \ + --hash=sha256:57863d16187995c10fe9cf911b897ed443ac68189179541734502353af33e693 \ + --hash=sha256:5953391af1405f968eb5701ebbb577ebc5ced8d0041406f9052638bafe52209d \ + --hash=sha256:5beffdbe766cfe4fb04f30644d822a1080b5359df7db3a63d30fa928375b2720 \ + --hash=sha256:5e360188b72f8080fefa3adfdcf3618604cc8173651c9754f189fece068d2a45 \ + --hash=sha256:5e58b61dcbb483a442c6239c3836696b79f2cd8e7eec11e12155d3f6f2d886d1 \ + --hash=sha256:69084fd29bfeff14816666c93a466e85414fe6b7d236cfc108a9c11afa6f7301 \ + --hash=sha256:6d1d7539043b2b31307f2c6c72957a97c839a88b2629a348ebabe5aa8b626d6b \ + --hash=sha256:6d8b735c4d162dc7d86a9cf3d717f14b6c73637a1f9cd57fe7e61002d9cb1972 \ + --hash=sha256:6ea961a674172ed2235d990d7edf85d15d8dfa23ab8575e48306371c070cda67 \ + --hash=sha256:71157f9db7f6bc6599a852852f3389343bea34315b4e6f109e5cbc97c1fb2963 \ + --hash=sha256:720f3108fb1bfa32e51db58b832898372eb5891e8472a8093008010911e324c5 \ + --hash=sha256:74129d5ffc4cde992d89d345f7f7d6758320e5d44a369d74d83493429dad2de5 \ + --hash=sha256:747251e428406b05fc86fee3904ee19550c4d2d19258cef274e2151f31ae9d38 \ + --hash=sha256:75130df05aae7a7ac171b3b5b24714cffeabd054ad2ebc18870b3aa4526eba23 \ + --hash=sha256:7b3661e6d4ba63a094138032c1356d557de5b3ea6fd3cca62a195f623e381c76 \ + --hash=sha256:7d5c7e32f3ee42f77d8ff1a10384b5cdcc2d37035e2e3320ded909aa192d32c3 \ + --hash=sha256:8124101e92c56827bebef084ff106e8ea11c743256149a95b9fd860d3a4f331f \ + --hash=sha256:81db2e7282cc0487f500d4db203edc57da81acde9e35f061d69ed983228ffe3b \ + --hash=sha256:840e18c38098221ea6201f091fc5d4de6128961d2930fbbc96806fb43f69aec1 \ + --hash=sha256:89cc8921a4a5028d6dd388c399fcd2eef232e7040345af3d5b16c04b91cf3c7e \ + --hash=sha256:8b32cd4ab6db50c875001ba4f5a6b30c0f42151aa1fbf9c2e7e3674893fb1dc4 \ + --hash=sha256:8df1c283e57c9cb4d271fdc1875f4a58a143a2d1698eb0d6b7c0d7d5f49c53a1 \ + --hash=sha256:902cf4739458852fe917104365ec0efbea7d29a15e4276c96a8d33e6ed8ec137 \ + --hash=sha256:97fbb77eaeb97591efdc654b8b5f3ccc066406ccfb3175b41382f221ecc216e8 \ + --hash=sha256:9c7042488165f7251dc7894cd533a875d2875af6d3b0e09eda9c4b334627ad1c \ + --hash=sha256:9e318e6786b1e750a62f90c6f7fa8b542102bdcf97c7c4de2a48b50b61bd36ec \ + --hash=sha256:a9421b23c85f361a133aa7c5e8ec757668f70343f4ed8fdb5a4a14abd5437244 \ + --hash=sha256:aaf71f95b21f9dc708123335df22e5a2fef6307e3e6f9ed773b2e0938cc4d491 \ + --hash=sha256:afedc35fe4b9e30ab240b208bb9dc8938cb4afe9187589e8d8d085e1aacb8309 \ + --hash=sha256:b5e28e56143750808c1c79c70a16519e9bc0a68b623197b96292b21b62d6055c \ + --hash=sha256:b82c9514c6d74b89a370c4060bdb80d2299bc6857e462e4a215b4ef7aa7b090e \ + --hash=sha256:b8f78398e67a7227aefa95f876481485403eb974b29e9dc38b307bb6eb2315ea \ + --hash=sha256:bbda75f245caecff8faa7e32ee94dfaa8312a3367397975527f29654cd17a6ed \ + --hash=sha256:bca34e913d27401bda2a6f390d0614049f5a95b3b11cd8eff80fe4ec340a1208 \ + --hash=sha256:bd04d8cab16cab5b0a9ffc7d10f0779cf1120ab16c3925404428f74a0a43205a \ + --hash=sha256:c149a652aeac4902ecff2dd93c3b2681c608bd5208c793c4a99404b3e1afc87c \ + --hash=sha256:c2087dbb76a87ec2c619253e021e4fb20d1a72580feeaa6892b0b3d955175a71 \ + --hash=sha256:c34f751bf67cab69638564eee34023909380ba3e0d8ee7f6fe473079bf93f09b \ + --hash=sha256:c6d20c8896c00775e6f62d8373aba32956aa0b850d02b5ec493f486c88e12859 \ + --hash=sha256:c7af6f7b80f687b33a4cdb0a785a5d4de1fb027a44c9a049d8eb67d5bfe8a687 \ + --hash=sha256:c7b07959866a6afb019abb9564d8a55046feb7a84506c74a6f197cbcdf8a208e \ + --hash=sha256:ca0dda0c5715efe2ab35bb83f813f681ebcd2840d8b1b92bfc6fe3ab382fae4a \ + --hash=sha256:cdb7eb3cf3deb3dd9e7b8749323b5d970052711f9e1e9f36364163627f96da58 \ + --hash=sha256:ce757c7c90d35719b38fa3d4ca55654a76a40716ee299b0865f2de21c146801c \ + --hash=sha256:d1fa67ef839bad3815124f5f57e48cd50ff392f4911a9f3cf449d66fa3df62a5 \ + --hash=sha256:d2dbd8f4990d4788cb122f63bf000357533f34860d269c1a8e90ae362090ff3a \ + --hash=sha256:d4ec0046facab83012d821b33cead742a35b54575c4edfb7ed7445f63441835f \ + --hash=sha256:dbceedcf4a9329cc665452db1aaf0845b85c666e4885b92ee0cddb1dbf7e052a \ + --hash=sha256:dc733d35f861f8d78abfaf54035461e10423422999b360966bf1c443cbc42705 \ + --hash=sha256:dd635c2c4043222d80d80ca1ac4530a633102a9f2ad12252183bcf338c1b9474 \ + --hash=sha256:de1f7cd5b6b351e1afd7568bdab94934d656abe273d66cda0ceea43bbc02a0c2 \ + --hash=sha256:df7c841813f6265e636fe548a49664c77af31ddfa0085515326342a751a6ba51 \ + --hash=sha256:e0f9d268b19e8f61bf42a1da48276bcd05f7ab5560311f541d22557f8227b866 \ + --hash=sha256:e2d66eb41ffca6cc3c91d8387509d27ba73ad28371ef90255c50cb51f8953301 \ + --hash=sha256:e429fc517a1c5e2a70d576077231538a98d59a45dfc552d1ac45a132844e6dfb \ + --hash=sha256:e4d2b88efe65544a7d5121b0c3b003ebba92bfede2ea3577ce548b69c5235185 \ + --hash=sha256:e76c902d229a3aa9d5ceb813e1cbcc69bf5bda44c80d574ff1ac1fa3136dea71 \ + --hash=sha256:ef07a0a1d254eeb16455d839cef6e8c2ed127f47f014bbda64a58b5482b6c836 \ + --hash=sha256:f09529d2332264a902688031a83c19de8fda5eb5881e44233286b9c9ec91856d \ + --hash=sha256:f0a6d4a93d2a05daec7cb885157c97bbb0be4da739d6f9dfb02e101eb40921cd \ + --hash=sha256:f0cf2a0dbb5987da4bd92a7ca727eadb225581dd9681365beba9accbe5308f7d \ + --hash=sha256:f2671cb47e50a97f419a02cd1e0c339b31de017b033186358db92f4d8e2e17d8 \ + --hash=sha256:f35b34a5184d5e0cc360b61664c1c06e866aab077b5a7c538a3e20c8fcdbf90b \ + --hash=sha256:f3d73022990ab0c8b172cce57c69fd9a89c24fd473a5e79cbce92df87e3d9c48 \ + --hash=sha256:f5b8353ea1a4d7dfb59a7f45c04df66ecfd363bb5b35f33b11ea579111d4655f \ + --hash=sha256:f809a17cc78bd331e137caa25262b507225854073fd319e987bd216bed911b7c \ + --hash=sha256:f9bc4161bd3b970cd6a6fcda70583ad4afd10f2750609fb1f3ca9505050d4ef3 \ + --hash=sha256:fdf4890cda3b59170009d012fca3294c00140e7f2abe1910e6a730809d0f3f9b SQLAlchemy==2.0.31; python_full_version >= "3.10.0" and python_full_version < "4.0.0" \ --hash=sha256:0b0f658414ee4e4b8cbcd4a9bb0fd743c5eeb81fc858ca517217a8013d282c96 \ --hash=sha256:2196208432deebdfe3b22185d46b08f00ac9d7b01284e168c212919891289396 \ From 4b9e9ca576f8ed7e1c168ee4f1049bd33b623175 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:48:50 -0300 Subject: [PATCH 4/5] chore(deps): update dependency pyjwt to v2.9.0 (#1832) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 07ef7546..1d8ef5f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -517,9 +517,9 @@ pydantic==2.8.0 ; python_full_version >= "3.10.0" and python_full_version < "4.0 pygithub==2.3.0 ; python_full_version >= "3.10.0" and python_full_version < "4.0.0" \ --hash=sha256:0148d7347a1cdeed99af905077010aef81a4dad988b0ba51d4108bf66b443f7e \ --hash=sha256:65b499728be3ce7b0cd2cd760da3b32f0f4d7bc55e5e0677617f90f6564e793e -pyjwt[crypto]==2.8.0 ; python_full_version >= "3.10.0" and python_full_version < "4.0.0" \ - --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ - --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 +PyJWT[crypto]==2.9.0; python_full_version >= "3.10.0" and python_full_version < "4.0.0" \ + --hash=sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850 \ + --hash=sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c pynacl==1.5.0 ; python_full_version >= "3.10.0" and python_full_version < "4.0.0" \ --hash=sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858 \ --hash=sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d \ From 070afeaf5fb83e0f02fe6d4456c92506b488356a Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 5 Aug 2024 08:11:19 +0200 Subject: [PATCH 5/5] GitHub App authentication (#1830) * add config options for github_app_* * add github app auth * add/adapt integration tests for github app auth * fix scope of fixture * pass env vars in tox * add unit test to ensure coverage * change tests to run additionally for github app auth * add reason to skipif * skip some tests for github app auth * fix if condition * bump minor version * update docs * Kick off CI build * try out github app on cbartz-org/cbartz-repo-policy-compliance-tests * update README on test repository requirements * skip non-applicable tests * Revert "try out github app on cbartz-org/cbartz-repo-policy-compliance-tests" This reverts commit 5a9ce5104664df5760a6f42e223157b7ab744943. * cleanup * update docs * use AuthMode enum and remove asserts * try out github app on cbartz-org/cbartz-repo-policy-compliance-tests * Revert "try out github app on cbartz-org/cbartz-repo-policy-compliance-tests" This reverts commit 6db8f175b09bc6c0117e91b2bd51bbeed649e92c. --- .github/workflows/tests.yaml | 5 +- README.md | 14 +- charm/charmcraft.yaml | 26 +++ charm/docs/reference/github-auth.md | 30 ++++ charm/docs/reference/token-permissions.md | 20 --- pyproject.toml | 2 +- repo_policy_compliance/github_client.py | 167 ++++++++++++++++-- rockcraft.yaml | 2 +- src-docs/github_client.py.md | 20 ++- tests/app/integration/conftest.py | 117 +++++++++++- tests/app/integration/test_blueprint.py | 12 +- tests/app/integration/test_github_client.py | 4 + tests/app/integration/test_pull_request.py | 24 ++- .../test_target_branch_protection.py | 11 +- tests/app/unit/test_github_client.py | 87 ++++++++- tox.ini | 2 +- 16 files changed, 482 insertions(+), 61 deletions(-) create mode 100644 charm/docs/reference/github-auth.md delete mode 100644 charm/docs/reference/token-permissions.md diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 43ef75ff..b5583fe4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -18,7 +18,10 @@ jobs: - name: Run tests (unit + integration) id: run-tests env: - GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} + AUTH_GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} + AUTH_GITHUB_APP_ID : ${{ secrets.TEST_GITHUB_APP_ID }} + AUTH_GITHUB_APP_INSTALLATION_ID : ${{ secrets.TEST_GITHUB_APP_INSTALLATION_ID }} + AUTH_GITHUB_APP_PRIVATE_KEY : ${{ secrets.TEST_GITHUB_APP_PRIVATE_KEY }} run: | # Ensure that stdout appears as normal and redirect to file and exit depends on exit code of first command STDOUT_LOG=$(mktemp --suffix=stdout.log) diff --git a/README.md b/README.md index de47bacd..e0d125ff 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,21 @@ failing check to be used for testing purposes. There are two types of test: the application test and the charm test. ### Application tests -To run the application tests, the `GITHUB_TOKEN` environment variable must be set. This +To run the application tests, the `AUTH_GITHUB_TOKEN` environment variable must be set. This should be a token of a user with full repo permissions for the test repository. +You can also pass in `AUTH_APP_ID`, `AUTH_INSTALLATION_ID`, and `AUTH_PRIVATE_KEY` +to test the authentication using GitHub App Auth. In that case, the tests will additionally +be executed using GitHub app auth. Note that the GitHub app should be installed +in the test repository organisation/user namespace, with access granted to the test repository. + The command `tox -e test` can be used to run all tests, which are primarily integration tests. You can also select the repository against which to run the tests by setting the `--repository` flag. The tests will fork the repository and create PRs against it. Note that the tests are currently designed to work for specific Canonical repositories, -and may need to be for other repositories +and may need to be adapted for other repositories (e.g. `tests.app.integration.test_target_branch_protection.test_fail` -assumes that certain collaborators are in the `users_bypass_pull_request_allowances` list). +assumes that certain collaborators are in the `users_bypass_pull_request_allowances` list). +The test repository must also have a branch protection defined for the main branch. Also note that the forks are created in the personal space of the user whose token is being used, and that the forks are not deleted after the run. The reason for this is that it is only possible to create one fork of a repository, @@ -66,6 +72,8 @@ bot to test things like comments from a user with no write permissions or above. GitHub actions should have access to the GitHub token via a secret called `PERSONAL_GITHUB_TOKEN`. It is recommended to use either a fine-grained PAT or a token that is short-lived, e.g. 7 days. When it expires, a new token must be set. +For the GitHub App Auth, the `TEST_GITHUB_APP_ID`, `TEST_GIHUB_APP_INSTALLATION_ID`, and +`TEST_GITHUB_APP_PRIVATE_KEY` should be set as secrets. ### Charm tests diff --git a/charm/charmcraft.yaml b/charm/charmcraft.yaml index 608f46df..65c21d37 100644 --- a/charm/charmcraft.yaml +++ b/charm/charmcraft.yaml @@ -48,9 +48,35 @@ config: write and higher permissions for the repository to run jobs from forks. type: boolean default: false + github_app_id: + description: >- + The app or client ID of the GitHub App to use for communication with GitHub. + If provided, the other github_app_* options must also be provided. + The Github App needs to have read permission for Administration. If private repositories + are checked, the Github App does also need read permission for Contents and Pull request. + Either this or the github_token must be provided. + type: string + github_app_installation_id: + description: >- + The installation ID of the GitHub App to use for communication with GitHub. + If provided, the other github_app_* options must also be provided. + The Github App needs to have read permission for Administration. If private repositories + are checked, the Github App does also need read permission for Contents and Pull request. + Either this or the github_token must be provided. + type: string + github_app_private_key: + # this will become a juju user secret once paas-app-charmer supports it + description: >- + The private key of the GitHub App to use for communication with GitHub. + If provided, the other github_app_* options must also be provided. + The Github App needs to have read permission for Administration. If private repositories + are checked, the Github App does also need read permission for Contents and Pull request. + Either this or the github_token must be provided. + type: string github_token: description: >- The token to use for communication with GitHub. This can be a PAT (with repo scope) or a fine-grained token with read permission for Administration. If private repositories are checked, the fine-grained token does also need read permission for Contents and Pull request. + Either this or the GitHub App configuration must be provided. diff --git a/charm/docs/reference/github-auth.md b/charm/docs/reference/github-auth.md new file mode 100644 index 00000000..5df8c0e4 --- /dev/null +++ b/charm/docs/reference/github-auth.md @@ -0,0 +1,30 @@ +# GitHub Authentication + +This section describes the GitHub authentication options available for the charm. + +You can either choose to use + +- classic personal access tokens +- fine-grained personal access tokens +- a GitHub app + +for authentication. The latter two options are recommended for better security and access control. +They require the fine-grained permissions as mentioned below. + +**Note**: If you are using a personal access tokens rather than a GitHub app, +the user who owns the token must have administrative access to the organisation or repository, +in addition to having a token with the necessary permissions. + + +## Classic personal access token scopes + +If you want to use classic personal access tokens, you will need to select the `repo` +scope when generating them. + +## Fine grained permissions + +For fine-grained access control, the following repository permissions are required: + +- Administration: read +- Contents: read (if you want to check private repositories) +- Pull requests: read (if you want to check private repositories) \ No newline at end of file diff --git a/charm/docs/reference/token-permissions.md b/charm/docs/reference/token-permissions.md deleted file mode 100644 index 239d6eb4..00000000 --- a/charm/docs/reference/token-permissions.md +++ /dev/null @@ -1,20 +0,0 @@ -# GitHub Token Permissions - -You can either choose to use a personal access token (PAT) or a fine-grained access token for the -`github_token` configuration. The token permissions/scopes are different for each type of token. - - -## Fine grained access token permissions - -**Note**: In addition to having a token with the necessary permissions, the user who owns the -token also must have admin access to the organisation or repository. - -For fine-grained access control, the following repository permissions are required: - -- Administration: read -- Contents: read (if you want to check private repositories) -- Pull requests: read (if you want to check private repositories) - -## Personal access token scopes - -If you want to use classic PATS, you will need to select the `repo` scope. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 78b21a12..130deca5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.poetry] name = "repo-policy-compliance" -version = "1.9.2" +version = "1.10.0" description = "Checks GitHub repository settings for compliance with policy" authors = ["Canonical IS DevOps "] license = "Apache 2.0" diff --git a/repo_policy_compliance/github_client.py b/repo_policy_compliance/github_client.py index 3b62a4e9..a4fdace3 100644 --- a/repo_policy_compliance/github_client.py +++ b/repo_policy_compliance/github_client.py @@ -2,15 +2,16 @@ # See LICENSE file for licensing details. """Module for GitHub client.""" - +import enum import functools import logging import os +from enum import Enum from typing import Callable, Concatenate, Literal, ParamSpec, TypeVar, cast from urllib import parse from github import BadCredentialsException, Github, GithubException, RateLimitExceededException -from github.Auth import Token +from github.Auth import AppAuth, AppInstallationAuth, Auth, Token from github.Branch import Branch from github.Repository import Repository from urllib3 import Retry @@ -27,23 +28,49 @@ # Bandit thinks this constant is the real Github token GITHUB_TOKEN_ENV_NAME = "GITHUB_TOKEN" # nosec +GITHUB_APP_ID_ENV_NAME = "GITHUB_APP_ID" +GITHUB_APP_INSTALLATION_ID_ENV_NAME = "GITHUB_APP_INSTALLATION_ID" +GITHUB_APP_PRIVATE_KEY_ENV_NAME = "GITHUB_APP_PRIVATE_KEY" + +MISSING_GITHUB_CONFIG_ERR_MSG = ( + f"Either the {GITHUB_TOKEN_ENV_NAME} or not all of {GITHUB_APP_ID_ENV_NAME}," + f" {GITHUB_APP_INSTALLATION_ID_ENV_NAME}, {GITHUB_APP_PRIVATE_KEY_ENV_NAME} " + f"environment variables were provided or are empty, " + "the variables are needed for interactions with GitHub, " +) +NOT_ALL_GITHUB_APP_CONFIG_ERR_MSG = ( + f"Not all of {GITHUB_APP_ID_ENV_NAME}, {GITHUB_APP_INSTALLATION_ID_ENV_NAME}," + f" {GITHUB_APP_PRIVATE_KEY_ENV_NAME} environment variables were provided, " +) +# the following is no hardcoded password +PROVIDED_GITHUB_TOKEN_AND_APP_CONFIG_ERR_MSG = ( # nosec + "Provided github app config and github token, only one of them should be provided, " +) + + +class _AuthMode(Enum): + """Enum to represent the auth mode to use. + + Attributes: + TOKEN: Using GitHub token auth. + APP: Using GitHub App auth. + """ + + TOKEN = enum.auto() + APP = enum.auto() def get() -> Github: """Get a GitHub client. Returns: - A GitHub client that is configured with a token from the environment. + A GitHub client that is configured with a token or GitHub app from the environment. Raises: - ConfigurationError: If the GitHub token environment variable is not provided or empty. - """ - github_token = os.getenv(GITHUB_TOKEN_ENV_NAME) or os.getenv(f"FLASK_{GITHUB_TOKEN_ENV_NAME}") - if not github_token: - raise ConfigurationError( - f"The {GITHUB_TOKEN_ENV_NAME} environment variable was not provided or empty, " - f"it is needed for interactions with GitHub, got: {github_token!r}" - ) + ConfigurationError: If the GitHub auth config is not valid. + """ # noqa: DCO051 error raised is useful to know for the user of the public interface + auth = _get_auth() + # Only retry on 5xx and only retry once after 20 secs retry_config = Retry( total=1, @@ -53,7 +80,123 @@ def get() -> Github: raise_on_status=False, raise_on_redirect=False, ) - return Github(auth=Token(github_token), retry=retry_config) + return Github(auth=auth, retry=retry_config) + + +def _get_auth() -> Auth: + """Get a GitHub auth object. + + Returns: + A GitHub auth object that is configured with a token from the environment. + """ + github_token = os.getenv(GITHUB_TOKEN_ENV_NAME) or os.getenv(f"FLASK_{GITHUB_TOKEN_ENV_NAME}") + github_app_id = os.getenv(GITHUB_APP_ID_ENV_NAME) or os.getenv( + f"FLASK_{GITHUB_APP_ID_ENV_NAME}" + ) + github_app_installation_id_str = os.getenv(GITHUB_APP_INSTALLATION_ID_ENV_NAME) or os.getenv( + f"FLASK_{GITHUB_APP_INSTALLATION_ID_ENV_NAME}" + ) + github_app_private_key = os.getenv(GITHUB_APP_PRIVATE_KEY_ENV_NAME) or os.getenv( + f"FLASK_{GITHUB_APP_PRIVATE_KEY_ENV_NAME}" + ) + + auth_mode = _get_auth_mode( + github_token=github_token, + github_app_id=github_app_id, + github_app_installation_id_str=github_app_installation_id_str, + github_app_private_key=github_app_private_key, + ) + + auth: Auth + if auth_mode == _AuthMode.APP: + auth = _get_github_app_installation_auth( + github_app_id=cast(str, github_app_id), + github_app_installation_id_str=cast(str, github_app_installation_id_str), + github_app_private_key=cast(str, github_app_private_key), + ) + else: + assert github_token is not None # nosec + auth = Token(github_token) + + return auth + + +def _get_auth_mode( + github_token: str | None, + github_app_id: str | None, + github_app_installation_id_str: str | None, + github_app_private_key: str | None, +) -> _AuthMode: + """Get the auth mode to use. + + Args: + github_token: The GitHub token. + github_app_id: The GitHub App ID or Client ID. + github_app_installation_id_str: The GitHub App Installation ID as a string. + github_app_private_key: The GitHub App private key. + + Raises: + ConfigurationError: If the configuration is not valid, e.g. if both a token and app config + are provided. + + Returns: + The auth mode to use. + """ + if not github_token and not ( + github_app_id or github_app_installation_id_str or github_app_private_key + ): + raise ConfigurationError( + f"{MISSING_GITHUB_CONFIG_ERR_MSG}" + f"got: {github_token!r}, {github_app_id!r}," + f" {github_app_installation_id_str!r}, {github_app_private_key!r}" + ) + if github_token and ( + github_app_id or github_app_installation_id_str or github_app_private_key + ): + raise ConfigurationError( + f"{PROVIDED_GITHUB_TOKEN_AND_APP_CONFIG_ERR_MSG}" + f"got: {github_token!r}, {github_app_id!r}, {github_app_installation_id_str!r}," + f" {github_app_private_key!r}" + ) + + if github_app_id or github_app_installation_id_str or github_app_private_key: + if not (github_app_id and github_app_installation_id_str and github_app_private_key): + raise ConfigurationError( + f"{NOT_ALL_GITHUB_APP_CONFIG_ERR_MSG}" + f"got: {github_app_id!r}, {github_app_installation_id_str!r}, " + f"{github_app_private_key!r}" + ) + + if github_token: + return _AuthMode.TOKEN + return _AuthMode.APP + + +def _get_github_app_installation_auth( + github_app_id: str, github_app_installation_id_str: str, github_app_private_key: str +) -> AppInstallationAuth: + """Get a GitHub App Installation Auth object. + + Args: + github_app_id: The GitHub App ID or Client ID. + github_app_installation_id_str: The GitHub App Installation ID as a string. + github_app_private_key: The GitHub App private key. + + Returns: + A GitHub App Installation Auth object. + + Raises: + ConfigurationError: If the GitHub App Installation Auth config is not valid. + """ + try: + github_app_installation_id = int(github_app_installation_id_str) + except ValueError as exc: + raise ConfigurationError( + f"Invalid github app installation id {github_app_installation_id_str!r}, " + f"it should be an integer." + ) from exc + app_auth = AppAuth(app_id=github_app_id, private_key=github_app_private_key) + return AppInstallationAuth(app_auth=app_auth, installation_id=github_app_installation_id) def inject(func: Callable[Concatenate[Github, P], R]) -> Callable[P, R]: diff --git a/rockcraft.yaml b/rockcraft.yaml index d7790164..a5fa6d34 100644 --- a/rockcraft.yaml +++ b/rockcraft.yaml @@ -3,7 +3,7 @@ name: repo-policy-compliance base: ubuntu@22.04 -version: '1.9.2' +version: '1.10.0' summary: Check the repository setup for policy compliance description: | Used to check whether a GitHub repository complies with expected policies. diff --git a/src-docs/github_client.py.md b/src-docs/github_client.py.md index 09a5cb7a..1ed8fff3 100644 --- a/src-docs/github_client.py.md +++ b/src-docs/github_client.py.md @@ -8,10 +8,16 @@ Module for GitHub client. **Global Variables** --------------- - **GITHUB_TOKEN_ENV_NAME** +- **GITHUB_APP_ID_ENV_NAME** +- **GITHUB_APP_INSTALLATION_ID_ENV_NAME** +- **GITHUB_APP_PRIVATE_KEY_ENV_NAME** +- **MISSING_GITHUB_CONFIG_ERR_MSG** +- **NOT_ALL_GITHUB_APP_CONFIG_ERR_MSG** +- **PROVIDED_GITHUB_TOKEN_AND_APP_CONFIG_ERR_MSG** --- - + ## function `get` @@ -24,18 +30,18 @@ Get a GitHub client. **Returns:** - A GitHub client that is configured with a token from the environment. + A GitHub client that is configured with a token or GitHub app from the environment. **Raises:** - - `ConfigurationError`: If the GitHub token environment variable is not provided or empty. + - `ConfigurationError`: If the GitHub auth config is not valid. --- - + ## function `inject` @@ -59,7 +65,7 @@ Injects a GitHub client as the first argument to a function. --- - + ## function `get_collaborators` @@ -89,7 +95,7 @@ Get collaborators with a given affiliation and permission. --- - + ## function `get_branch` @@ -119,7 +125,7 @@ Get the branch for the check. --- - + ## function `get_collaborator_permission` diff --git a/tests/app/integration/conftest.py b/tests/app/integration/conftest.py index e3a7396f..410d0c7a 100644 --- a/tests/app/integration/conftest.py +++ b/tests/app/integration/conftest.py @@ -2,8 +2,11 @@ # See LICENSE file for licensing details. """Fixtures for integration tests.""" - +import enum +import logging import os +from collections import namedtuple +from enum import Enum from typing import Iterator, cast import pytest @@ -16,13 +19,109 @@ from github.Repository import Repository import repo_policy_compliance -from repo_policy_compliance.github_client import GITHUB_TOKEN_ENV_NAME, get_collaborators -from repo_policy_compliance.github_client import inject as inject_github_client +from repo_policy_compliance.github_client import ( + GITHUB_APP_ID_ENV_NAME, + GITHUB_APP_INSTALLATION_ID_ENV_NAME, + GITHUB_APP_PRIVATE_KEY_ENV_NAME, + GITHUB_TOKEN_ENV_NAME, + get_collaborators, +) from ...conftest import REPOSITORY_ARGUMENT_NAME from . import branch_protection from .types_ import BranchWithProtection, RequestedCollaborator +logger = logging.getLogger(__name__) + +TEST_GITHUB_APP_ID_ENV_NAME = f"AUTH_{GITHUB_APP_ID_ENV_NAME}" +TEST_GITHUB_APP_INSTALLATION_ID_ENV_NAME = f"AUTH_{GITHUB_APP_INSTALLATION_ID_ENV_NAME}" +TEST_GITHUB_APP_PRIVATE_KEY_ENV_NAME = f"AUTH_{GITHUB_APP_PRIVATE_KEY_ENV_NAME}" +TEST_GITHUB_TOKEN_ENV_NAME = f"AUTH_{GITHUB_TOKEN_ENV_NAME}" + + +class AuthenticationMethod(Enum): + """The authentication method to use. + + Attributes: + GITHUB_TOKEN: Use GitHub token authentication. + GITHUB_APP: Use GitHub App authentication. + """ + + GITHUB_TOKEN = enum.auto() + GITHUB_APP = enum.auto() + + +_AuthenticationMethodParams = namedtuple( + "_AuthenticationMethodParams", ["app_id", "installation_id", "private_key", "github_token"] +) + + +@pytest.fixture( + scope="function", + name="github_auth", + autouse=True, + params=[ + pytest.param( + _AuthenticationMethodParams( + github_token=os.getenv(TEST_GITHUB_TOKEN_ENV_NAME), + app_id=None, + installation_id=None, + private_key=None, + ), + id="Using GitHub Token authentication", + ), + pytest.param( + _AuthenticationMethodParams( + app_id=os.getenv(TEST_GITHUB_APP_ID_ENV_NAME), + installation_id=os.getenv(TEST_GITHUB_APP_INSTALLATION_ID_ENV_NAME), + private_key=os.getenv(TEST_GITHUB_APP_PRIVATE_KEY_ENV_NAME), + github_token=None, + ), + marks=pytest.mark.skipif( + not all( + [ + os.getenv(TEST_GITHUB_APP_ID_ENV_NAME), + os.getenv(TEST_GITHUB_APP_INSTALLATION_ID_ENV_NAME), + os.getenv(TEST_GITHUB_APP_PRIVATE_KEY_ENV_NAME), + ] + ), + reason="GitHub App Auth environment variables are not set", + ), + id="Using GitHub App authentication", + ), + ], +) +def fixture_github_auth( + request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch +) -> AuthenticationMethod: + """Setup the GitHub authentication method. + + We want to test with GitHub Token authentication and optionally GitHub App authentication, + if the environment variables are set. + This is achieved by monkeypatching the respective environment variables. + + Returns: + The authentication method to use. + """ + app_id = request.param.app_id + app_install_id = request.param.installation_id + app_private_key = request.param.private_key + github_token = request.param.github_token + + auth_method = AuthenticationMethod.GITHUB_TOKEN + + if app_id: + monkeypatch.setenv(GITHUB_APP_ID_ENV_NAME, app_id) + auth_method = AuthenticationMethod.GITHUB_APP + if app_install_id: + monkeypatch.setenv(GITHUB_APP_INSTALLATION_ID_ENV_NAME, app_install_id) + if app_private_key: + monkeypatch.setenv(GITHUB_APP_PRIVATE_KEY_ENV_NAME, app_private_key) + if github_token: + monkeypatch.setenv(GITHUB_TOKEN_ENV_NAME, github_token) + + return auth_method + @pytest.fixture(scope="session", name="github_repository_name") def fixture_github_repository_name(pytestconfig: pytest.Config) -> str: @@ -31,10 +130,12 @@ def fixture_github_repository_name(pytestconfig: pytest.Config) -> str: @pytest.fixture(scope="session", name="github_token") -def fixutre_github_token() -> str: +def fixture_github_token() -> str: """Get the GitHub token from the environment.""" - github_token = os.getenv(GITHUB_TOKEN_ENV_NAME) - assert github_token, f"GitHub must be set in the environment variable {GITHUB_TOKEN_ENV_NAME}" + github_token = os.getenv(TEST_GITHUB_TOKEN_ENV_NAME) + assert ( + github_token + ), f"GitHub token must be set in the environment variable {TEST_GITHUB_TOKEN_ENV_NAME}" return github_token @@ -64,9 +165,9 @@ def fixture_ci_github_repository( @pytest.fixture(scope="session", name="github_repository") -def fixture_github_repository(github_repository_name: str) -> Repository: +def fixture_github_repository(github_repository_name: str, github_token: str) -> Repository: """Returns client to the Github repository.""" - github_client = inject_github_client(lambda client: client)() + github_client = Github(auth=Token(github_token)) return github_client.get_repo(github_repository_name) diff --git a/tests/app/integration/test_blueprint.py b/tests/app/integration/test_blueprint.py index 19bf7371..9771aceb 100644 --- a/tests/app/integration/test_blueprint.py +++ b/tests/app/integration/test_blueprint.py @@ -19,6 +19,7 @@ from repo_policy_compliance import blueprint, github_client, policy from tests import assert_ +from .conftest import AuthenticationMethod from .types_ import RequestedCollaborator EXPECTED_PULL_REQUEST_KEYS = ( @@ -786,13 +787,21 @@ def test_always_fail(client: FlaskClient, runner_token: str): @pytest.mark.parametrize( "invalid_token", [pytest.param("", id="empty"), pytest.param("invalid", id="invalid")] ) -def test_health_fail(client: FlaskClient, invalid_token: str, monkeypatch: pytest.MonkeyPatch): +def test_health_fail( + client: FlaskClient, + invalid_token: str, + monkeypatch: pytest.MonkeyPatch, + github_auth: AuthenticationMethod, +): """ arrange: given flask application with the blueprint registered and invalid token set in GITHUB_TOKEN environment variable act: when the health check endpoint is requested assert: then 500 is returned. """ + if github_auth == AuthenticationMethod.GITHUB_APP: + pytest.skip("This test is not applicable for GitHub App authentication.") + monkeypatch.setenv(github_client.GITHUB_TOKEN_ENV_NAME, invalid_token) response = client.get(blueprint.HEALTH_ENDPOINT) @@ -864,6 +873,7 @@ def test_internal_server_error( assert: 500 error is returned with reason. """ monkeypatch.setenv(github_client.GITHUB_TOKEN_ENV_NAME, "") + monkeypatch.setenv(github_client.GITHUB_APP_PRIVATE_KEY_ENV_NAME, "") main_branch = github_repository.get_branch(github_repository.default_branch) request_data = { "repository_name": github_repository.full_name, diff --git a/tests/app/integration/test_github_client.py b/tests/app/integration/test_github_client.py index 114c9ffc..93ecafaf 100644 --- a/tests/app/integration/test_github_client.py +++ b/tests/app/integration/test_github_client.py @@ -7,6 +7,7 @@ from repo_policy_compliance.check import Result, target_branch_protection from repo_policy_compliance.github_client import GITHUB_TOKEN_ENV_NAME +from tests.app.integration.conftest import AuthenticationMethod @pytest.mark.parametrize( @@ -34,12 +35,15 @@ def test_github_token( fail_reason: str, github_repository_name: str, monkeypatch: pytest.MonkeyPatch, + github_auth: AuthenticationMethod, ): """ arrange: A github repository name and a missing or invalid github token. act: when the github client is injected to target_branch_protection. assert: An expected error is raised with a specific error message. """ + if github_auth == AuthenticationMethod.GITHUB_APP: + pytest.skip("This test is not applicable for GitHub App authentication.") monkeypatch.setenv(GITHUB_TOKEN_ENV_NAME, str(github_token_value)) # The github_client is injected report = target_branch_protection( # pylint: disable=no-value-for-parameter diff --git a/tests/app/integration/test_pull_request.py b/tests/app/integration/test_pull_request.py index 031d0006..ba257a78 100644 --- a/tests/app/integration/test_pull_request.py +++ b/tests/app/integration/test_pull_request.py @@ -2,7 +2,6 @@ # See LICENSE file for licensing details. """Tests for the pull_request function.""" - from uuid import uuid4 import pytest @@ -12,6 +11,7 @@ from repo_policy_compliance import PullRequestInput, UsedPolicy, policy, pull_request from repo_policy_compliance.check import Result +from .conftest import AuthenticationMethod from .types_ import BranchWithProtection, RequestedCollaborator @@ -57,19 +57,27 @@ def test_invalid_policy(): ], indirect=["github_branch", "protected_github_branch"], ) -@pytest.mark.usefixtures("protected_github_branch") -def test_pull_request_disallow_fork( +@pytest.mark.usefixtures("protected_github_branch") # All the arguments are required for the test +def test_pull_request_disallow_fork( # pylint: disable=too-many-arguments github_branch: Branch, github_repository_name: str, forked_github_repository: Repository, policy_enabled: bool, expected_result: Result, + github_auth: AuthenticationMethod, ): """ arrange: given a forked repository and a disable_fork policy enabled/disabled. act: when pull_request is called assert: then a expected result is returned. """ + # this test requires the github auth method to have access to the personal fork + # which would require a separate installation id for the app auth to be passed to the test, + # which is currently not supported (few tests which requires it so the overhead + # of adding it is not worth it) + if github_auth == AuthenticationMethod.GITHUB_APP: + pytest.skip("This test requires a personal fork to be accessible by the Github App Auth.") + policy_document = { policy.JobType.PULL_REQUEST: { policy.PullRequestProperty.DISALLOW_FORK: {policy.ENABLED_KEY: policy_enabled}, @@ -226,7 +234,7 @@ def test_collaborators( indirect=["github_branch", "protected_github_branch", "forked_github_branch"], ) @pytest.mark.usefixtures("protected_github_branch", "make_fork_from_non_collaborator") -# All the arguments are required for the test +# we use a lot of arguments, but it seems not worth to introduce a capsulating object for this def test_execute_job( # pylint: disable=too-many-arguments github_branch: Branch, github_repository_name: str, @@ -234,6 +242,7 @@ def test_execute_job( # pylint: disable=too-many-arguments forked_github_repository: Repository, policy_enabled: bool, expected_result: Result, + github_auth: AuthenticationMethod, ): """ arrange: given a target and repository that is compliant and a source branch that is a fork and @@ -241,6 +250,13 @@ def test_execute_job( # pylint: disable=too-many-arguments act: when pull_request is called with the policy assert: then the expected report is returned. """ + # this test requires the github auth method to have access to the personal fork + # which would require a separate installation id for the app auth to be passed to the test, + # which is currently not supported (few tests which requires it so the overhead + # of adding it is not worth it) + if github_auth == AuthenticationMethod.GITHUB_APP: + pytest.skip("This test requires a personal fork to be accessible by the Github App Auth.") + policy_document = { policy.JobType.PULL_REQUEST: { policy.PullRequestProperty.EXECUTE_JOB: {policy.ENABLED_KEY: policy_enabled}, diff --git a/tests/app/integration/test_target_branch_protection.py b/tests/app/integration/test_target_branch_protection.py index 8f03c29e..6877a2a1 100644 --- a/tests/app/integration/test_target_branch_protection.py +++ b/tests/app/integration/test_target_branch_protection.py @@ -13,6 +13,7 @@ from repo_policy_compliance.check import Result, target_branch_protection from tests import assert_ +from .conftest import AuthenticationMethod from .types_ import BranchWithProtection @@ -118,13 +119,21 @@ def test_pass( def test_fail_default_branch( - forked_github_repository: Repository, caplog: pytest.LogCaptureFixture + forked_github_repository: Repository, + caplog: pytest.LogCaptureFixture, + github_auth: AuthenticationMethod, ): """ arrange: given a default branch branch that is not compliant. act: when target_branch_protection is called with the name of the branch. assert: then a fail report is returned. """ + # this test requires the github auth method to have access to the personal fork + # which would require a separate installation id for the app auth to be passed to the test, + # which is currently not supported (few tests which requires it so the overhead + # of adding it is not worth it) + if github_auth == AuthenticationMethod.GITHUB_APP: + pytest.skip("This test requires a personal fork to be accessible by the Github App Auth.") default_branch = forked_github_repository.get_branch(forked_github_repository.default_branch) default_branch.edit_protection() default_branch.remove_required_pull_request_reviews() diff --git a/tests/app/unit/test_github_client.py b/tests/app/unit/test_github_client.py index 63207455..0b2ff57e 100644 --- a/tests/app/unit/test_github_client.py +++ b/tests/app/unit/test_github_client.py @@ -7,11 +7,12 @@ import pytest from github import BadCredentialsException, Github, GithubException, RateLimitExceededException +from github.Auth import AppInstallationAuth from github.Repository import Repository import repo_policy_compliance.github_client from repo_policy_compliance.check import Result, target_branch_protection -from repo_policy_compliance.exceptions import GithubClientError +from repo_policy_compliance.exceptions import ConfigurationError, GithubClientError GITHUB_REPOSITORY_NAME = "test/repository" GITHUB_BRANCH_NAME = "arbitrary" @@ -74,3 +75,87 @@ def test_get_collaborator_permission_error(): mock_repository, "test_user" ) assert "Invalid collaborator permission" in str(error.value) + + +@pytest.mark.parametrize( + "github_app_id, github_app_installation_id, github_app_private_key, github_token, " + "expected_message", + [ + pytest.param( + "123", + "456", + "private", + "github_token", + repo_policy_compliance.github_client.PROVIDED_GITHUB_TOKEN_AND_APP_CONFIG_ERR_MSG, + id="github app config and github token", + ), + pytest.param( + None, + None, + None, + None, + repo_policy_compliance.github_client.MISSING_GITHUB_CONFIG_ERR_MSG, + id="no github app config or github token", + ), + pytest.param( + "eda", + "no int", + "private", + None, + "Invalid github app installation id", + id="invalid github app installation id", + ), + pytest.param( + "eda", + "123", + None, + None, + repo_policy_compliance.github_client.NOT_ALL_GITHUB_APP_CONFIG_ERR_MSG, + id="not all github app config provided", + ), + ], +) # we use a lot of arguments, but it seems not worth to introduce a capsulating object for this +def test_get_client_configuration_error( # pylint: disable=too-many-arguments + github_app_id: str, + github_app_installation_id: str, + github_app_private_key: str, + github_token: str, + expected_message: str, + monkeypatch: pytest.MonkeyPatch, +): + """ + arrange: Given a mocked environment with invalid github auth configuration. + act: Call github_client.get. + assert: ConfigurationError is raised. + """ + if github_app_id: + monkeypatch.setenv("GITHUB_APP_ID", github_app_id) + if github_app_installation_id: + monkeypatch.setenv("GITHUB_APP_INSTALLATION_ID", github_app_installation_id) + if github_app_private_key: + monkeypatch.setenv("GITHUB_APP_PRIVATE_KEY", github_app_private_key) + if github_token: + monkeypatch.setenv("GITHUB_TOKEN", github_token) + + with pytest.raises(ConfigurationError) as error: + # The github_client is injected + repo_policy_compliance.github_client.get() + assert expected_message in str(error.value) + + +def test_get_client_github_app_auth(monkeypatch: pytest.MonkeyPatch): + """ + arrange: Given a mocked environment with github app configuration and a mocked Github object. + act: Call github_client.get. + assert: The auth parameter of the Github object is an instance of AppInstallationAuth. + """ + monkeypatch.setenv("GITHUB_APP_ID", "123") + monkeypatch.setenv("GITHUB_APP_INSTALLATION_ID", "456") + monkeypatch.setenv("GITHUB_APP_PRIVATE_KEY", "private") + github_class_mock = MagicMock(spec=Github) + monkeypatch.setattr(repo_policy_compliance.github_client, "Github", github_class_mock) + + repo_policy_compliance.github_client.get() + github_class_mock.assert_called_once() + auth = github_class_mock.call_args[1]["auth"] + assert isinstance(auth, AppInstallationAuth) diff --git a/tox.ini b/tox.ini index 1ce10327..0a8f3b2b 100644 --- a/tox.ini +++ b/tox.ini @@ -71,7 +71,7 @@ commands = pylint {[vars]all_path} [testenv:test] -passenv = GITHUB_TOKEN, CI_GITHUB_TOKEN +passenv = AUTH_GITHUB_TOKEN, AUTH_GITHUB_APP_ID, AUTH_GITHUB_APP_INSTALLATION_ID, AUTH_GITHUB_APP_PRIVATE_KEY, CI_GITHUB_TOKEN description = Run tests deps = coverage[toml]>=6,<7