mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-25 15:12:16 +00:00
Compare commits
397 Commits
rr/partial
...
move_ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fab814d6c6 | ||
|
|
d5ff2f4447 | ||
|
|
80e5eb5b56 | ||
|
|
7f125a102a | ||
|
|
29e67b6171 | ||
|
|
e8176e412a | ||
|
|
921935271b | ||
|
|
9690e9cd68 | ||
|
|
d854ec7009 | ||
|
|
664e390006 | ||
|
|
de89d8cf99 | ||
|
|
fe687e6af2 | ||
|
|
4197e1ec2e | ||
|
|
3cee20b94a | ||
|
|
4785d9acff | ||
|
|
466ce7c808 | ||
|
|
c1fcd4c9ab | ||
|
|
346bbd95b8 | ||
|
|
08950fea60 | ||
|
|
b788a19b22 | ||
|
|
78192f4590 | ||
|
|
09f4db6d30 | ||
|
|
70ef10db2b | ||
|
|
3bb039bbd3 | ||
|
|
cc62c413e0 | ||
|
|
849ceaa55d | ||
|
|
558e6c9fa0 | ||
|
|
fc77610c74 | ||
|
|
9bf7ca31c9 | ||
|
|
29747328e1 | ||
|
|
417cb412ac | ||
|
|
27ca077fef | ||
|
|
7bfc83b64f | ||
|
|
4ed51edd7d | ||
|
|
342081dba2 | ||
|
|
c70c90dad7 | ||
|
|
261ff9e693 | ||
|
|
9872d6b87c | ||
|
|
223641d255 | ||
|
|
1fee6f0a87 | ||
|
|
edcfb295a4 | ||
|
|
77c5a06141 | ||
|
|
35a597f9b4 | ||
|
|
aad8083dc1 | ||
|
|
873cc34d4f | ||
|
|
2f00a6b126 | ||
|
|
74da0d1190 | ||
|
|
e4d2c8ba07 | ||
|
|
2c5c2b50c0 | ||
|
|
6089d5cde4 | ||
|
|
26f4b69fb1 | ||
|
|
06c0a82c65 | ||
|
|
0dbcf798e2 | ||
|
|
43f2d592e9 | ||
|
|
19e17b88a1 | ||
|
|
1adfa95f00 | ||
|
|
e3ec5de2b2 | ||
|
|
f3bf60135c | ||
|
|
c2e0a04e54 | ||
|
|
f886eec2c5 | ||
|
|
ca0bd600dc | ||
|
|
6fcbfcf306 | ||
|
|
f7680c87a8 | ||
|
|
b6a4908e8c | ||
|
|
c086fbbc1e | ||
|
|
1518d1fba7 | ||
|
|
a297481096 | ||
|
|
9f77c2e351 | ||
|
|
46b0bdf600 | ||
|
|
417ccce6fc | ||
|
|
8dc0dc9a81 | ||
|
|
d0bd7cc3de | ||
|
|
e48880e010 | ||
|
|
b725da52c3 | ||
|
|
b2cce27eae | ||
|
|
17617f872e | ||
|
|
334ce30a29 | ||
|
|
fb46a66b53 | ||
|
|
d8503afd75 | ||
|
|
8b4e42298c | ||
|
|
fee022f9dc | ||
|
|
f5512e0e0f | ||
|
|
25b371097d | ||
|
|
01cbb5b505 | ||
|
|
5adb8a7367 | ||
|
|
94179bf90b | ||
|
|
aafe93be6a | ||
|
|
4041118191 | ||
|
|
5ca16e1ed1 | ||
|
|
181ac60e24 | ||
|
|
98113cb641 | ||
|
|
54fc5a7d5f | ||
|
|
8f784f0bb4 | ||
|
|
42fee40c36 | ||
|
|
2a4ee95882 | ||
|
|
55ace667b5 | ||
|
|
f32a631bad | ||
|
|
66430acb1c | ||
|
|
42f7ec8282 | ||
|
|
d23bdc88e4 | ||
|
|
d2b29f1b66 | ||
|
|
6bf2de398a | ||
|
|
43f1bbf3f2 | ||
|
|
87a1073f28 | ||
|
|
52f3d803be | ||
|
|
ac03fed343 | ||
|
|
a3ff75828e | ||
|
|
862a234f64 | ||
|
|
386ace50e9 | ||
|
|
e371d5c8b2 | ||
|
|
7ff5115277 | ||
|
|
4f877766b5 | ||
|
|
a76222b121 | ||
|
|
91439f9b9b | ||
|
|
a906451dee | ||
|
|
2e32c27f80 | ||
|
|
34cec020a2 | ||
|
|
617187cfbb | ||
|
|
de70584460 | ||
|
|
73c9be5e2a | ||
|
|
e64f40fa7b | ||
|
|
2e09369804 | ||
|
|
80953b02d0 | ||
|
|
26c4363418 | ||
|
|
ad2cb85543 | ||
|
|
00ae1ef4e9 | ||
|
|
a065356a54 | ||
|
|
6202d703cb | ||
|
|
244fa1d474 | ||
|
|
6bbda7b7db | ||
|
|
ba75de77f9 | ||
|
|
7bae68d16a | ||
|
|
59fc775c56 | ||
|
|
09bfcb3c5a | ||
|
|
b99d093979 | ||
|
|
b16c13845d | ||
|
|
a1235857d3 | ||
|
|
b25a2bc015 | ||
|
|
467396215b | ||
|
|
a71b93d4f8 | ||
|
|
6bcaf5255f | ||
|
|
ade3aa7e1a | ||
|
|
12ec989f15 | ||
|
|
82ea7faff6 | ||
|
|
7b3efc4e58 | ||
|
|
0c7b8350ae | ||
|
|
ba8ef3fbc1 | ||
|
|
b46cf40bad | ||
|
|
66dab6f64e | ||
|
|
9b3b148d6f | ||
|
|
7b8e0f0e0d | ||
|
|
6c1e29c2f1 | ||
|
|
e28181634e | ||
|
|
e07b9fc1e2 | ||
|
|
18835ed1be | ||
|
|
d6fc3c3160 | ||
|
|
7d32a91e62 | ||
|
|
6956bd3f61 | ||
|
|
a082608827 | ||
|
|
b7620fb40c | ||
|
|
8e8d61086a | ||
|
|
4d91e39b36 | ||
|
|
4bbae25452 | ||
|
|
6e911bb1ef | ||
|
|
058b122bb3 | ||
|
|
a78812acb5 | ||
|
|
d7abafd44a | ||
|
|
02ae5132a3 | ||
|
|
89bb9ec2e1 | ||
|
|
f4547da79d | ||
|
|
39e67f56b1 | ||
|
|
80cdfb938c | ||
|
|
2376ebce99 | ||
|
|
2023061ae2 | ||
|
|
792a3e5ada | ||
|
|
6020200423 | ||
|
|
00ee3b6296 | ||
|
|
35b4d04ab0 | ||
|
|
f9d3da45c4 | ||
|
|
07a5c06501 | ||
|
|
d382f91ee0 | ||
|
|
ebb40d7101 | ||
|
|
6404dcd266 | ||
|
|
5d3d4c8865 | ||
|
|
1acd1b7998 | ||
|
|
3430006aa4 | ||
|
|
7872d7fef2 | ||
|
|
fb2c6d2125 | ||
|
|
d1e6738239 | ||
|
|
01051e9de0 | ||
|
|
61c11e268d | ||
|
|
840744da70 | ||
|
|
0a3107c38e | ||
|
|
c40123e415 | ||
|
|
92c9335aae | ||
|
|
b28cfcaf05 | ||
|
|
296f41a462 | ||
|
|
c9c974fcab | ||
|
|
5c7288c1e2 | ||
|
|
de564c0c63 | ||
|
|
10d3f3c219 | ||
|
|
c14a4a2bf4 | ||
|
|
d3a2fbd192 | ||
|
|
29c8b191d3 | ||
|
|
ebba823c0b | ||
|
|
ec5c8d90f3 | ||
|
|
f573d0f0e9 | ||
|
|
0508d05e0f | ||
|
|
23541830ef | ||
|
|
75134f939b | ||
|
|
e0be76857e | ||
|
|
3bd23f1a3b | ||
|
|
d16d47c89a | ||
|
|
be9f564f4b | ||
|
|
c0da4d337c | ||
|
|
948e64df7e | ||
|
|
d4d800a22c | ||
|
|
09b687ecdb | ||
|
|
e401a5b184 | ||
|
|
15e917f86b | ||
|
|
4a1600787c | ||
|
|
58bee91e28 | ||
|
|
a855788c5f | ||
|
|
649e3d4e5d | ||
|
|
ec4747524a | ||
|
|
6eff5d8900 | ||
|
|
5ca43848ae | ||
|
|
ab5f59602f | ||
|
|
8aaccd356e | ||
|
|
4b9285cecd | ||
|
|
2f8c956510 | ||
|
|
1cca662e54 | ||
|
|
b3f3b9a298 | ||
|
|
da7d630c44 | ||
|
|
85fd6ca95b | ||
|
|
f74a1684f4 | ||
|
|
8841975bf1 | ||
|
|
13762286b8 | ||
|
|
a621e52fc2 | ||
|
|
12760d7706 | ||
|
|
c1b3ac7254 | ||
|
|
96007ae056 | ||
|
|
6682e1271f | ||
|
|
c2d5e4367d | ||
|
|
3aa78eb33f | ||
|
|
c4fafb89ea | ||
|
|
9e10c3a290 | ||
|
|
ab939fc4e2 | ||
|
|
f973afcc80 | ||
|
|
b653f5d6f2 | ||
|
|
299f9ead0b | ||
|
|
c823856098 | ||
|
|
8da9854b42 | ||
|
|
ea3228b226 | ||
|
|
ed18119a55 | ||
|
|
d2a0ee82ef | ||
|
|
4540e5be3a | ||
|
|
d810c27775 | ||
|
|
8eee1aae51 | ||
|
|
6e3abf198e | ||
|
|
51b2363031 | ||
|
|
b45dc3ddd4 | ||
|
|
34d2c4709e | ||
|
|
4c14ad3299 | ||
|
|
e134edcc41 | ||
|
|
0c95f0304d | ||
|
|
5505f5745c | ||
|
|
6918deef71 | ||
|
|
2504a3cc8a | ||
|
|
1ed53cf630 | ||
|
|
aa258f1991 | ||
|
|
b87893420b | ||
|
|
0acd38fa7d | ||
|
|
a4d091c857 | ||
|
|
a4113243d9 | ||
|
|
faf3394c46 | ||
|
|
a026244d5a | ||
|
|
4596d47d6c | ||
|
|
b9984d2f92 | ||
|
|
8e6ecb9466 | ||
|
|
26ccd824a4 | ||
|
|
cbe99119a2 | ||
|
|
8ca0f319a1 | ||
|
|
ee439d0ba9 | ||
|
|
28fbf5029a | ||
|
|
a860c85c71 | ||
|
|
3cdbc5b09b | ||
|
|
cbabfbe942 | ||
|
|
cbea626d96 | ||
|
|
171ec457f1 | ||
|
|
793559c67c | ||
|
|
b9fe22f3a5 | ||
|
|
b5e67330b1 | ||
|
|
6fad7ef3c2 | ||
|
|
0f3ae1cf22 | ||
|
|
73664b5d19 | ||
|
|
36bc1a9d65 | ||
|
|
04b870d288 | ||
|
|
f45f8cf2dc | ||
|
|
4e1aa9fa05 | ||
|
|
093c56f24e | ||
|
|
9960c80351 | ||
|
|
45bc187ee4 | ||
|
|
434b1e2818 | ||
|
|
b0904132c4 | ||
|
|
d376fd318f | ||
|
|
2d62a03bb2 | ||
|
|
8e72e0e20b | ||
|
|
48a90ae2dc | ||
|
|
40c15acde9 | ||
|
|
3033f87e41 | ||
|
|
6450d0a307 | ||
|
|
985137df8d | ||
|
|
fe3cd8cabe | ||
|
|
25cbd87648 | ||
|
|
1cf10bbdc6 | ||
|
|
02e0b0e2c7 | ||
|
|
a41a296b55 | ||
|
|
45b3f4ec8f | ||
|
|
82461287a4 | ||
|
|
10f005fc7d | ||
|
|
81ff7ce484 | ||
|
|
33c2cdc9d9 | ||
|
|
f1e3ba9eca | ||
|
|
7bf68cbf8e | ||
|
|
38adfff384 | ||
|
|
98a0e42f20 | ||
|
|
ca0f632790 | ||
|
|
08f0bdead4 | ||
|
|
313b16530a | ||
|
|
8b28d01bfe | ||
|
|
16a27bddf7 | ||
|
|
4467f11d93 | ||
|
|
e1464c379c | ||
|
|
9446262246 | ||
|
|
e187065900 | ||
|
|
ee942a1263 | ||
|
|
e290bf19e6 | ||
|
|
6254bbccd4 | ||
|
|
3b6b3534dc | ||
|
|
290227762c | ||
|
|
fb5e550057 | ||
|
|
78b9707fde | ||
|
|
7b3ae89420 | ||
|
|
db1fcfe116 | ||
|
|
0423e49ffa | ||
|
|
6829c3095d | ||
|
|
4c3d6721a5 | ||
|
|
5f9d2bb7d9 | ||
|
|
4639b4e7c3 | ||
|
|
7137399964 | ||
|
|
f1181fce54 | ||
|
|
63e03cded9 | ||
|
|
76489c840c | ||
|
|
e7845e2cd9 | ||
|
|
a10c8946a9 | ||
|
|
055f5e4082 | ||
|
|
001f130cb1 | ||
|
|
df87953a41 | ||
|
|
99172cfea9 | ||
|
|
2acdfecc9b | ||
|
|
f7b1aae324 | ||
|
|
2b5b80ee6b | ||
|
|
15efa88933 | ||
|
|
acb590679c | ||
|
|
a0c65fe9bd | ||
|
|
b02861bfea | ||
|
|
42b0982ae6 | ||
|
|
393aa59608 | ||
|
|
5a3048745f | ||
|
|
e4f5bbe3d2 | ||
|
|
1a55b7aa44 | ||
|
|
62fab47791 | ||
|
|
914efc79d1 | ||
|
|
761c2c10ed | ||
|
|
ba3dd1c855 | ||
|
|
eb9c914548 | ||
|
|
3b67364b5c | ||
|
|
dcf28baef8 | ||
|
|
667cf31ea1 | ||
|
|
fa22cfe2d5 | ||
|
|
b27aa974cd | ||
|
|
027ac23760 | ||
|
|
9f58db799f | ||
|
|
b829d05346 | ||
|
|
d1e382e8e4 | ||
|
|
34304b936f | ||
|
|
a11c1a7092 | ||
|
|
d7858b8fb4 | ||
|
|
72f38c101d | ||
|
|
15be3b0eb0 | ||
|
|
69a0823db2 | ||
|
|
7321d323ee | ||
|
|
98eacbacb9 | ||
|
|
99f16bdcc6 | ||
|
|
436e838778 | ||
|
|
11bd69cf2d |
@@ -1,26 +0,0 @@
|
||||
# This is effectively a copy of the .gitignore file.
|
||||
# The whole git repo, including the .git directory, should get copied into the Docker build context,
|
||||
# to enable the use of hack/get-ldflags.sh inside the Dockerfile.
|
||||
# When you change the .gitignore file, please consider also changing this file.
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# GoLand
|
||||
.idea
|
||||
|
||||
# MacOS Desktop Services Store
|
||||
.DS_Store
|
||||
|
||||
# Hugo temp file
|
||||
.hugo_build.lock
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,3 +0,0 @@
|
||||
*.go.tmpl linguist-language=Go
|
||||
hack/Dockerfile_fips linguist-language=Dockerfile
|
||||
generated/** linguist-generated
|
||||
36
.github/ISSUE_TEMPLATE/add_new_k8s_version.md
vendored
36
.github/ISSUE_TEMPLATE/add_new_k8s_version.md
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: Add new K8s version
|
||||
about: 'Checklist for maintainers to add new K8s minor version'
|
||||
title: 'Add new K8s version vX.X'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Note: Please update the issue title to include the new Kubernetes version number. -->
|
||||
|
||||
# Adding a new Kubernetes Version
|
||||
|
||||
## `pinniped's ci branch`
|
||||
|
||||
- [ ] Update `dockerfile-builders` pipeline
|
||||
- [ ] Update `pull-requests` pipeline
|
||||
- [ ] Update `main` pipeline
|
||||
|
||||
## `pinniped`
|
||||
|
||||
- [ ] Bump all golang dependencies (especially the `k8s.io` dependencies to use the new minor version).
|
||||
- [ ] Be sure to verify that everything compiles and unit tests pass locally. This is probably a good starting point.
|
||||
```shell
|
||||
./hack/update-go-mod/update-go-mod.sh
|
||||
./hack/module.sh unit
|
||||
./hack/prepare-for-integration-tests.sh
|
||||
```
|
||||
- [ ] Log in to github as pinniped-ci-bot, then go to [this page](https://github.com/pinniped-ci-bot?tab=packages) and change the settings for the new `k8s-code-generator-1.*` image to be publicly visible
|
||||
- [ ] Add the new K8s version to `hack/lib/kube-versions.txt` and run code generation.
|
||||
|
||||
## General Tasks
|
||||
|
||||
- [ ] Consider dropping support for any older versions of Kubernetes
|
||||
- [ ] Create stories or chores to take advantage of features in the new Kubernetes version
|
||||
- [ ] Close this issue
|
||||
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Explain a problem you are experiencing
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Hey! Thanks for opening an issue!
|
||||
|
||||
IMPORTANT: If you believe this bug is a security issue, please don't use this template and follow our [security guidelines](/doc/security.md).
|
||||
|
||||
It is recommended that you include screenshots and logs to help everyone achieve a shared understanding of the bug.
|
||||
|
||||
-->
|
||||
|
||||
**What happened?**
|
||||
|
||||
> Please be specific and include screenshots and logs!
|
||||
|
||||
**What did you expect to happen?**
|
||||
|
||||
> Please be specific and include proposed behavior!
|
||||
|
||||
**What is the simplest way to reproduce this behavior?**
|
||||
|
||||
**In what environment did you see this bug?**
|
||||
- Pinniped server version:
|
||||
- Pinniped client version:
|
||||
- Pinniped container image (if using a public container image):
|
||||
- Pinniped configuration (what IDP(s) are you using? what downstream credential minting mechanisms are you using?):
|
||||
- Kubernetes version (use `kubectl version`):
|
||||
- Kubernetes installer & version (e.g., `kubeadm version`):
|
||||
- Cloud provider or hardware configuration:
|
||||
- OS (e.g: `cat /etc/os-release`):
|
||||
- Kernel (e.g. `uname -a`):
|
||||
- Others:
|
||||
|
||||
**What else is there to know about this bug?**
|
||||
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,39 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a way to improve this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Hey! Thanks for opening an issue!
|
||||
|
||||
It is recommended that you include screenshots and logs to help everyone achieve a shared understanding of the improvement.
|
||||
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Are you considering submitting a PR for this feature?**
|
||||
|
||||
- **How will this project improvement be tested?**
|
||||
- **How does this change the current architecture?**
|
||||
- **How will this change be backwards compatible?**
|
||||
- **How will this feature be documented?**
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
34
.github/ISSUE_TEMPLATE/proposal_tracking.md
vendored
34
.github/ISSUE_TEMPLATE/proposal_tracking.md
vendored
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: Proposal tracking
|
||||
about: A tracking issue for a proposal document
|
||||
title: '[Proposal] Your proposal title'
|
||||
labels: 'proposal-tracking'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Hey! Thanks for opening an issue!
|
||||
|
||||
This type of issue should only be opened if you intend to create a
|
||||
formal proposal document. Please refer to the proposal process in
|
||||
[proposals/README.md](proposals/README.md).
|
||||
|
||||
Please title this issue starting with `[Proposal]` followed by a
|
||||
title for what you are going to propose. For example:
|
||||
`[Proposal] Lunar landing module authentication via Pinniped`.
|
||||
|
||||
-->
|
||||
|
||||
### Proposal Tracking Issue
|
||||
|
||||
- Proposal: <!-- this starts empty, then please update to link to proposal PR, then also link to proposal doc file after it is merged -->
|
||||
|
||||
- Discussion Links: <!-- link to any mailing list threads, Slack conversations, community meetings, or other places where the proposal was discussed, if any -->
|
||||
- <!-- A -->
|
||||
- <!-- B -->
|
||||
|
||||
- Pull requests: <!-- link to all PRs related to this proposal such as updates to the proposal doc, implementation PRs, etc. - keep this list up to date -->
|
||||
- <!-- #123: briefly describe this PR -->
|
||||
- <!-- #456: briefly describe this PR -->
|
||||
33
.github/ISSUE_TEMPLATE/release_checklist.md
vendored
33
.github/ISSUE_TEMPLATE/release_checklist.md
vendored
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: Release checklist
|
||||
about: Checklist for maintainers to prepare for an upcoming release
|
||||
title: 'Release checklist for vX.X.X'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Note: Please update the issue title to include the planned release's version number. -->
|
||||
|
||||
# Release checklist
|
||||
|
||||
- [ ] Ensure that Pinniped's dependencies have been upgraded, to the extent desired by the team (refer to the diff output from the latest run of the [all-golang-deps-updated](https://ci.pinniped.broadcom.net/teams/main/pipelines/security-scan/jobs/all-golang-deps-updated/) CI job)
|
||||
- [ ] If you are updating golang in Pinniped, be sure to update golang in CI as well. Do a search-and-replace to update the version number everywhere in the pinniped `ci` branch.
|
||||
- [ ] If the Fosite library is being updated and the format of the content of the Supervisor's storage Secrets are changed, or if any change to our own code changes the format of the content of the Supervisor's session storage Secrets, then be sure to update the `accessTokenStorageVersion`, `authorizeCodeStorageVersion`, `oidcStorageVersion`, `pkceStorageVersion`, `refreshTokenStorageVersion`, variables in files such as `internal/fositestorage/accesstoken/accesstoken.go`. Failing tests should signal the need to update these values.
|
||||
- [ ] For go.mod direct dependencies that are v2 or above, such as `github.com/google/go-github/vXX`, check to see if there is a new major version available. Try using `hack/update-go-mod/update-majors.sh`.
|
||||
- [ ] Evaluate all `replace` directives in the `go.mod` file. Are those versions up-to-date? Can any `replace` directives be removed?
|
||||
- [ ] Evaluate all overrides in the `hack/update-go-mod/overrides.conf` file. Are those versions up-to-date? Can those overrides be removed?
|
||||
- [ ] Ensure that Pinniped's codegen is up-to-date with the latest Kubernetes releases by making sure this [file](https://github.com/vmware/pinniped/blob/main/hack/lib/kube-versions.txt) is updated compared to the latest releases listed [here for active branches](https://kubernetes.io/releases/) and [here for non-active branches](https://kubernetes.io/releases/patch-releases/#non-active-branch-history)
|
||||
- [ ] Ensure that the `k8s-code-generator` CI job definitions are up-to-date with the latest Go, K8s, and `controller-gen` versions
|
||||
- [ ] All relevant feature and docs PRs are merged
|
||||
- [ ] The [main pipeline](https://ci.pinniped.broadcom.net/teams/main/pipelines/main) is green, up to and including the `ready-to-release` job. Check that the expected git commit has passed the `ready-to-release` job.
|
||||
- [ ] Manually trigger the jobs `run-int-misc`, `run-int-cloud-providers`, and `run-int-k8s-versions` in the main pipeline to run other pre-release tests. Depending on the number of Concourse workers, you may need to run these one at a time.
|
||||
- [ ] Optional: a blog post for the release is written and submitted as a PR but not merged yet
|
||||
- [ ] All merged user stories are accepted (manually tested)
|
||||
- [ ] Only after all stories are accepted, manually trigger the `release` job to create a draft GitHub release
|
||||
- [ ] Manually edit the draft release notes on the [GitHub release](https://github.com/vmware/pinniped/releases) to describe the contents of the release, using the format which was automatically added to the draft release
|
||||
- [ ] Publish (i.e. make public) the draft release
|
||||
- [ ] After making the release public, the jobs in the [main pipeline](https://ci.pinniped.broadcom.net/teams/main/pipelines/main) beyond the release job should auto-trigger, so check to make sure that they passed
|
||||
- [ ] Edit the blog post's date to make it match the actual release date, and merge the blog post PR to make it live on the website
|
||||
- [ ] Publicize the release via tweets, etc.
|
||||
- [ ] Close this issue
|
||||
15
.github/codecov.yml
vendored
15
.github/codecov.yml
vendored
@@ -1,15 +0,0 @@
|
||||
codecov:
|
||||
strict_yaml_branch: main
|
||||
require_ci_to_pass: no
|
||||
notify:
|
||||
wait_for_ci: no
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
ignore:
|
||||
- cmd/local-user-authenticator/
|
||||
139
.github/dependabot.yml
vendored
139
.github/dependabot.yml
vendored
@@ -1,139 +0,0 @@
|
||||
# See https://docs.github.com/en/github/administering-a-repository/enabling-and-disabling-version-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
open-pull-requests-limit: 2
|
||||
directory: "/hack/update-go-mod"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
# Use dependabot to automate major-only dependency bumps
|
||||
- package-ecosystem: "gomod"
|
||||
open-pull-requests-limit: 2 # Not sure why there would ever be more than 1, just would not want to hide anything
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# group all major dependency bumps together so there's only one pull request
|
||||
groups:
|
||||
go-modules:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "major"
|
||||
ignore:
|
||||
# For all packages, ignore all minor and patch updates
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
- "version-update:semver-patch"
|
||||
|
||||
# Our own CI job is responsible for updating this Docker file now.
|
||||
# - package-ecosystem: "docker"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "daily"
|
||||
|
||||
# Our own CI job is responsible for updating this Docker file now.
|
||||
# - package-ecosystem: "docker"
|
||||
# directory: "/hack" # this should keep the FIPS dockerfile updated per https://github.com/dependabot/feedback/issues/145#issuecomment-414738498
|
||||
# schedule:
|
||||
# interval: "daily"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/code-coverage-uploader/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/crane/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/deployment-yaml-formatter/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/eks-deployer/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/gh-cli/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/integration-test-runner/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/integration-test-runner-beta/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/k8s-app-deployer/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/k8s-code-generator/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/pool-trigger-resource/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/test-bitnami-ldap/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/test-cfssl/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/test-dex/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/test-forward-proxy/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/dockerfiles/test-kubectl/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/pipelines/shared-helpers/test-binaries-image/"
|
||||
open-pull-requests-limit: 100
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: ci
|
||||
41
.github/pull_request_template.md
vendored
41
.github/pull_request_template.md
vendored
@@ -1,41 +0,0 @@
|
||||
<!--
|
||||
Thank you for submitting a pull request for Pinniped!
|
||||
|
||||
Before submitting, please see the guidelines in CONTRIBUTING.md in this repo.
|
||||
|
||||
Please note that a project maintainer will need to review and provide an
|
||||
initial approval on the PR to cause CI tests to automatically start.
|
||||
Also note that if you push additional commits to the PR, those commits
|
||||
will need another initial approval before CI will pick them up.
|
||||
|
||||
Reminder: Did you remember to run all the linter, unit tests, and integration tests
|
||||
described in CONTRIBUTING.md on your branch before submitting this PR?
|
||||
|
||||
Below is a template to help you describe your PR.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Provide a summary of your change. Feel free to use paragraphs or a bulleted list, for example:
|
||||
|
||||
- Improves performance by 10,000%.
|
||||
- Fixes all bugs.
|
||||
- Boils the oceans.
|
||||
|
||||
-->
|
||||
|
||||
<!--
|
||||
Does this PR fix one or more reported issues?
|
||||
If yes, use `Fixes #<issue number>` to automatically close the fixed issue(s) when the PR is merged.
|
||||
-->
|
||||
|
||||
**Release note**:
|
||||
|
||||
<!--
|
||||
Does this PR introduce a user-facing change?
|
||||
|
||||
If no, just write "NONE" in the release-note block below.
|
||||
If yes, a release note is required. Enter your extended release note in the block below.
|
||||
-->
|
||||
```release-note
|
||||
|
||||
```
|
||||
72
.github/workflows/codeql-analysis.yml
vendored
72
.github/workflows/codeql-analysis.yml
vendored
@@ -1,72 +0,0 @@
|
||||
# See https://codeql.github.com and https://github.com/github/codeql-action
|
||||
# This action runs GitHub's industry-leading semantic code analysis engine, CodeQL, against a
|
||||
# repository's source code to find security vulnerabilities. It then automatically uploads the
|
||||
# results to GitHub so they can be displayed in the repository's security tab.
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", release* ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '24 3 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript' ]
|
||||
|
||||
steps:
|
||||
# Checkout our repository.
|
||||
# See https://github.com/actions/checkout for documentation.
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# Install Go.
|
||||
# See https://github.com/actions/setup-go?tab=readme-ov-file#getting-go-version-from-the-gomod-file.
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
27
.gitignore
vendored
27
.gitignore
vendored
@@ -1,24 +1,7 @@
|
||||
# When you change this file, please consider also changing the .dockerignore file.
|
||||
# See comments at the top of .dockerignore for more information.
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# GoLand
|
||||
.idea
|
||||
|
||||
# MacOS Desktop Services Store
|
||||
.terraform
|
||||
*.tfstate.*
|
||||
*.tfstate
|
||||
kubeconfig.yaml
|
||||
.DS_Store
|
||||
|
||||
# Hugo temp file
|
||||
.hugo_build.lock
|
||||
site/
|
||||
167
.golangci.yaml
167
.golangci.yaml
@@ -1,167 +0,0 @@
|
||||
# https://golangci-lint.run/usage/configuration/
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- copyloopvar
|
||||
- dogsled
|
||||
- errcheck
|
||||
- exhaustive
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goheader
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- intrange
|
||||
- makezero
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- spancheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unused
|
||||
- whitespace
|
||||
settings:
|
||||
funlen:
|
||||
lines: 150
|
||||
statements: 50
|
||||
goheader:
|
||||
values:
|
||||
regexp:
|
||||
# YYYY or YYYY-YYYY
|
||||
YEARS: \d\d\d\d(-\d\d\d\d)?
|
||||
template: |-
|
||||
Copyright {{YEARS}} the Pinniped contributors. All Rights Reserved.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
importas:
|
||||
alias:
|
||||
- pkg: k8s.io/apimachinery/pkg/util/errors
|
||||
alias: utilerrors
|
||||
- pkg: k8s.io/apimachinery/pkg/api/errors
|
||||
alias: apierrors
|
||||
- pkg: k8s.io/apimachinery/pkg/apis/meta/v1
|
||||
alias: metav1
|
||||
- pkg: k8s.io/api/core/v1
|
||||
alias: corev1
|
||||
- pkg: github.com/coreos/go-oidc/v3/oidc
|
||||
alias: coreosoidc
|
||||
- pkg: github.com/ory/fosite/handler/oauth2
|
||||
alias: fositeoauth2
|
||||
- pkg: github.com/ory/fosite/token/jwt
|
||||
alias: fositejwt
|
||||
- pkg: github.com/go-jose/go-jose/v4/jwt
|
||||
alias: josejwt
|
||||
- pkg: github.com/go-jose/go-jose/v3
|
||||
alias: oldjosev3
|
||||
- pkg: go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1
|
||||
alias: authenticationv1alpha1
|
||||
- pkg: go.pinniped.dev/generated/latest/apis/supervisor/clientsecret/v1alpha1
|
||||
alias: clientsecretv1alpha1
|
||||
- pkg: go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1
|
||||
alias: supervisorconfigv1alpha1
|
||||
- pkg: go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1
|
||||
alias: conciergeconfigv1alpha1
|
||||
- pkg: go.pinniped.dev/generated/latest/client/concierge/clientset/versioned
|
||||
alias: conciergeclientset
|
||||
- pkg: go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/scheme
|
||||
alias: conciergeclientsetscheme
|
||||
- pkg: go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake
|
||||
alias: conciergefake
|
||||
- pkg: go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned
|
||||
alias: supervisorclientset
|
||||
- pkg: go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/scheme
|
||||
alias: supervisorclientsetscheme
|
||||
- pkg: go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake
|
||||
alias: supervisorfake
|
||||
- pkg: k8s.io/client-go/kubernetes/fake
|
||||
alias: kubefake
|
||||
- pkg: go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1
|
||||
alias: idpv1alpha1
|
||||
- pkg: go.pinniped.dev/generated/latest/client/concierge/informers/externalversions
|
||||
alias: conciergeinformers
|
||||
- pkg: go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions
|
||||
alias: supervisorinformers
|
||||
- pkg: go.pinniped.dev/internal/concierge/scheme
|
||||
alias: conciergescheme
|
||||
no-unaliased: true # All packages explicitly listed above must be aliased
|
||||
no-extra-aliases: false # Allow other aliases than the ones explicitly listed above
|
||||
revive:
|
||||
max-open-files: 2048
|
||||
rules:
|
||||
# Allow unused params that start with underscore. It can be nice to keep unused param names when implementing
|
||||
# an interface sometimes, to help readers understand why it is unused in that particular implementation.
|
||||
- name: unused-parameter
|
||||
arguments:
|
||||
- allowRegex: ^_
|
||||
spancheck:
|
||||
# https://golangci-lint.run/usage/linters/#spancheck
|
||||
checks:
|
||||
- end
|
||||
- record-error
|
||||
- set-status
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
# exclude tests from some rules for things that are useful in a testing context.
|
||||
- linters:
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- revive
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- revive
|
||||
path: internal/testutil/
|
||||
paths:
|
||||
- generated
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
settings:
|
||||
gofmt:
|
||||
# Simplify code: gofmt with `-s` option.
|
||||
# Default: true
|
||||
simplify: false
|
||||
# Apply the rewrite rules to the source before reformatting.
|
||||
# https://pkg.go.dev/cmd/gofmt
|
||||
# Default: []
|
||||
rewrite-rules:
|
||||
- pattern: interface{}
|
||||
replacement: any
|
||||
- pattern: a[b:len(a)]
|
||||
replacement: a[b:]
|
||||
goimports:
|
||||
local-prefixes:
|
||||
- go.pinniped.dev
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- generated
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
@@ -1,26 +1,23 @@
|
||||
# This is a configuration for https://pre-commit.com/.
|
||||
# On macOS, try `brew install pre-commit` and then run `pre-commit install`.
|
||||
exclude: '^(site|generated)/'
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
# TODO: find a version of this to validate ytt templates?
|
||||
# - id: check-yaml
|
||||
# args: ['--allow-multiple-documents']
|
||||
- id: check-json
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
exclude: 'securetls*' # prevent the linter from running in this file because it's not smart enough not to trim the nmap test output.
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
- id: check-byte-order-marker
|
||||
- id: detect-private-key
|
||||
exclude: testdata
|
||||
- id: mixed-line-ending
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-copyright-year
|
||||
name: Validate copyright year
|
||||
entry: hack/check-copyright-year.sh
|
||||
language: script
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
# TODO: find a version of this to validate ytt templates?
|
||||
# - id: check-yaml
|
||||
# args: ['--allow-multiple-documents']
|
||||
- id: check-json
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
- id: check-byte-order-marker
|
||||
- id: detect-private-key
|
||||
- id: mixed-line-ending
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-copyright-year
|
||||
name: Validate copyright year
|
||||
entry: hack/check-copyright-year.sh
|
||||
language: script
|
||||
|
||||
603
AD-SETUP.md
Normal file
603
AD-SETUP.md
Normal file
@@ -0,0 +1,603 @@
|
||||
# Creating an Active Directory server on Google Cloud for Pinniped integration tests
|
||||
|
||||
This documents the steps that were taken to create our test AD server used by the integration tests.
|
||||
The integration tests use LDAPS and StartTLS to connect to the AD server.
|
||||
|
||||
## Create a Windows Server VM and configure it as an AD Domain Controller
|
||||
|
||||
The steps in this section were mostly inspired by
|
||||
https://cloud.google.com/architecture/deploy-an-active-directory-forest-on-compute-engine.
|
||||
|
||||
From your Mac, create a VPC, subnet, firewall rules, admin password, reserved static IP, and the VM itself.
|
||||
|
||||
On your Mac:
|
||||
|
||||
```shell
|
||||
# Login as yourself.
|
||||
gcloud auth login
|
||||
|
||||
# Set some variables.
|
||||
project="REDACTED" # Change this to be the actual project name before running these commands.
|
||||
region="us-central1"
|
||||
zone="us-central1-b"
|
||||
vpc_name="ad"
|
||||
|
||||
# Create VPC.
|
||||
gcloud compute networks create ${vpc_name} \
|
||||
--project ${project} \
|
||||
--description "VPC network to deploy Active Directory" \
|
||||
--subnet-mode custom
|
||||
|
||||
# Create subnet.
|
||||
# The google tutorial says to "enable Private Google Access so that Windows can activate without internet access."
|
||||
gcloud compute networks subnets create domain-controllers \
|
||||
--project ${project} --region ${region} \
|
||||
--network ${vpc_name} \
|
||||
--range "10.0.0.0/28" \
|
||||
--enable-private-ip-google-access
|
||||
|
||||
# Create a firewall rule to allow RDP. Find out what your public IP address is by going to https://whatismyipaddress.com.
|
||||
# Replace the X.X.X.X placeholder address shown here with your real IPv4 address.
|
||||
my_ip=X.X.X.X
|
||||
gcloud compute firewall-rules create allow-rdp-ingress-to-addc \
|
||||
--project ${project} \
|
||||
--direction INGRESS \
|
||||
--action allow \
|
||||
--rules tcp:3389 \
|
||||
--source-ranges "${my_ip}/32" \
|
||||
--target-tags ad-domaincontroller \
|
||||
--network ${vpc_name} \
|
||||
--priority 10000
|
||||
|
||||
# Allow LDAPS (port 636) from the whole internet.
|
||||
gcloud compute firewall-rules create allow-ldaps-ingress-to-addc \
|
||||
--project ${project} \
|
||||
--direction INGRESS \
|
||||
--action allow \
|
||||
--rules tcp:636 \
|
||||
--source-ranges "0.0.0.0/0" \
|
||||
--target-tags ad-domaincontroller \
|
||||
--network ${vpc_name} \
|
||||
--priority 10000
|
||||
|
||||
# Allow LDAP (port 389) from the whole internet, to allow the integration tests to use StartTLS.
|
||||
gcloud compute firewall-rules create allow-ldap-ingress-to-addc \
|
||||
--project ${project} \
|
||||
--direction INGRESS \
|
||||
--action allow \
|
||||
--rules tcp:389 \
|
||||
--source-ranges "0.0.0.0/0" \
|
||||
--target-tags ad-domaincontroller \
|
||||
--network ${vpc_name} \
|
||||
--priority 10000
|
||||
|
||||
# Reserve a static public IP address for the domain controller VM.
|
||||
addressOfDc1=$(gcloud compute addresses create ad-domain-controller \
|
||||
--project ${project} --region ${region} \
|
||||
--format="value(address)")
|
||||
|
||||
# Create an admin password for the Administrator user on Windows, and save it to secrets manager.
|
||||
password="$(openssl rand -hex 8)-$(openssl rand -hex 8)"
|
||||
echo -n "$password" > password.tmp
|
||||
gcloud secrets create active-directory-dc1-password \
|
||||
--project ${project} \
|
||||
--data-file password.tmp
|
||||
rm password.tmp
|
||||
|
||||
# This creates a service account called ad-domaincontroller@PROJECT_NAME.iam.gserviceaccount.com
|
||||
# (where PROJECT_NAME is the actual GCP project name) and sets the account name to the
|
||||
# variable $dcServiceAccount.
|
||||
dcServiceAccount=$(gcloud iam service-accounts create ad-domaincontroller \
|
||||
--project ${project} \
|
||||
--display-name "AD Domain Controller VM Service Account" \
|
||||
--format "value(email)")
|
||||
|
||||
# Allow the new service account to temporarily read the Windows admin password from secret manager.
|
||||
# The following `date` command might only work on MacOS. It prints the time like this: 2024-10-23T19:20:36Z
|
||||
one_hour_from_now=$(TZ=UTC date -v "+1H" +"%Y-%m-%dT%H:%M:%SZ")
|
||||
gcloud secrets add-iam-policy-binding active-directory-dc1-password \
|
||||
--project ${project} \
|
||||
"--member=serviceAccount:$dcServiceAccount" \
|
||||
--role=roles/secretmanager.secretAccessor \
|
||||
--condition="title=Expires after 1h,expression=request.time < timestamp('$one_hour_from_now')"
|
||||
|
||||
# Optional: list all bindings to see the binding that you just created.
|
||||
gcloud secrets get-iam-policy active-directory-dc1-password \
|
||||
--project ${project}
|
||||
|
||||
# Create a powershell startup script in a local file.
|
||||
cat <<"EOF" > dc-startup.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
#
|
||||
# Only run the script if the VM is not a domain controller already.
|
||||
#
|
||||
if ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType -eq 2) {
|
||||
exit
|
||||
}
|
||||
|
||||
#
|
||||
# Read configuration from metadata.
|
||||
#
|
||||
Import-Module "${Env:ProgramFiles}\Google\Compute Engine\sysprep\gce_base.psm1"
|
||||
|
||||
Write-Host "Reading metadata..."
|
||||
$ActiveDirectoryDnsDomain = Get-MetaData -Property "attributes/ActiveDirectoryDnsDomain" -instance_only
|
||||
$ActiveDirectoryNetbiosDomain = Get-MetaData -Property "attributes/ActiveDirectoryNetbiosDomain" -instance_only
|
||||
$ProjectId = Get-MetaData -Property "project-id" -project_only
|
||||
$AccessToken = (Get-MetaData -Property "service-accounts/default/token" | ConvertFrom-Json).access_token
|
||||
|
||||
#
|
||||
# Read the DSRM password from secret manager.
|
||||
#
|
||||
Write-Host "Reading secret from secret manager..."
|
||||
$Secret = (Invoke-RestMethod `
|
||||
-Headers @{
|
||||
"Metadata-Flavor" = "Google";
|
||||
"x-goog-user-project" = $ProjectId;
|
||||
"Authorization" = "Bearer $AccessToken"} `
|
||||
-Uri "https://secretmanager.googleapis.com/v1/projects/$ProjectId/secrets/active-directory-dc1-password/versions/latest:access")
|
||||
$DsrmPassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Secret.payload.data))
|
||||
$DsrmPassword = ConvertTo-SecureString -AsPlainText $DsrmPassword -force
|
||||
|
||||
#
|
||||
# Promote.
|
||||
#
|
||||
Write-Host "Setting administrator password..."
|
||||
Set-LocalUser -Name Administrator -Password $DsrmPassword
|
||||
|
||||
Write-Host "Creating a new forest $ActiveDirectoryDnsDomain ($ActiveDirectoryNetbiosDomain)..."
|
||||
Install-ADDSForest `
|
||||
-DomainName $ActiveDirectoryDnsDomain `
|
||||
-DomainNetbiosName $ActiveDirectoryNetbiosDomain `
|
||||
-SafeModeAdministratorPassword $DsrmPassword `
|
||||
-DomainMode Win2008R2 `
|
||||
-ForestMode Win2008R2 `
|
||||
-InstallDns `
|
||||
-CreateDnsDelegation:$False `
|
||||
-NoRebootOnCompletion:$True `
|
||||
-Confirm:$false
|
||||
|
||||
#
|
||||
# Configure DNS.
|
||||
#
|
||||
Write-Host "Configuring DNS settings..."
|
||||
Get-Netadapter| Disable-NetAdapterBinding -ComponentID ms_tcpip6
|
||||
Set-DnsClientServerAddress `
|
||||
-InterfaceIndex (Get-NetAdapter -Name Ethernet).InterfaceIndex `
|
||||
-ServerAddresses 127.0.0.1
|
||||
|
||||
#
|
||||
# Enable LSA protection.
|
||||
#
|
||||
New-ItemProperty `
|
||||
-Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" `
|
||||
-Name "RunAsPPL" `
|
||||
-Value 1 `
|
||||
-PropertyType DWord
|
||||
|
||||
Write-Host "Restarting to apply all settings..."
|
||||
Restart-Computer
|
||||
EOF
|
||||
|
||||
# Create a domain controller VM.
|
||||
# E2 are the cheapest VMs. e2-medium has 2 vCPUs (shared with other customers) and 4 GB of memory.
|
||||
# See https://cloud.google.com/compute/docs/general-purpose-machines#e2-shared-core.
|
||||
# When we originally set up this VM, we actually started it as n2-standard-2 and after we
|
||||
# finished setting up everything as shown in this guide, then we stopped the VM and changed its
|
||||
# type to e2-medium and started the VM again. Maybe it would work fine to create it as
|
||||
# e2-medium from the beginning, but note that we didn't actually test that.
|
||||
gcloud compute instances create active-directory-dc1 \
|
||||
--project ${project} \
|
||||
--zone ${zone} \
|
||||
--image-family windows-2022 \
|
||||
--image-project windows-cloud \
|
||||
--machine-type e2-medium \
|
||||
--tags ad-domaincontroller \
|
||||
--metadata "ActiveDirectoryDnsDomain=activedirectory.test.pinniped.dev,ActiveDirectoryNetbiosDomain=pinniped-ad,sysprep-specialize-script-ps1=Install-WindowsFeature AD-Domain-Services -IncludeManagementTools; Install-WindowsFeature DNS,disable-account-manager=true" \
|
||||
--metadata-from-file windows-startup-script-ps1=dc-startup.ps1 \
|
||||
--address ${addressOfDc1} \
|
||||
--subnet=domain-controllers \
|
||||
--service-account "$dcServiceAccount" \
|
||||
--scopes cloud-platform \
|
||||
--shielded-integrity-monitoring \
|
||||
--shielded-secure-boot \
|
||||
--shielded-vtpm
|
||||
|
||||
# Monitor the initialization process of the first domain controller by viewing its serial port output.
|
||||
# It should install the sysprep stuff, reboot, run our startup script, and then reboot again.
|
||||
gcloud compute instances tail-serial-port-output active-directory-dc1 \
|
||||
--project ${project} \
|
||||
--zone ${zone}
|
||||
# Use CTRL-C to cancel tailing the output.
|
||||
```
|
||||
|
||||
## Update DNS
|
||||
|
||||
Update the Cloud DNS entry for `activedirectory.test.pinniped.dev.` to be an "A" record pointing to the
|
||||
public static IP of the VM. This is easier to do in the Cloud DNS UI in your browser.
|
||||
It would take many gcloud CLI commands to accomplish the same task.
|
||||
|
||||
## Configure test users and groups
|
||||
|
||||
Make sure you have an RDP client installed. On a Mac, you can install RDP from the App Store.
|
||||
It was recently renamed "Windows App".
|
||||
|
||||
Note: To copy/paste in the RDP client, you may need to use CTRL-C/CTRL-V if CMD-C/CMD-V don't work.
|
||||
|
||||
RDP into the Windows VM. To connect, use `activedirectory.test.pinniped.dev` as the name of the server,
|
||||
the username `Administrator`, and the password from the `active-directory-dc1-password` entry in Secrets Manager.
|
||||
You can ignore the RDP certificate error.
|
||||
|
||||
In your RDP session, open Powershell. Then run the following commands to add some users and groups,
|
||||
change the password policy, and grant some permissions.
|
||||
|
||||
Before running the commands, replace the redacted passwords as follows:
|
||||
- The value for `REDACTED_BIND_USER_PASSWORD` can be found at `aws-ad-bind-account-password` in the `concourse-secrets` secret
|
||||
- The value for `REDACTED_PINNY_USER_PASSWORD` can be found at `aws-ad-user-password` in the `concourse-secrets` secret
|
||||
- The value for `REDACTED_DEACTIVATED_USER_PASSWORD` can be found at `aws-ad-deactivated-user-password` in the `concourse-secrets` secret
|
||||
|
||||
```shell
|
||||
New-ADOrganizationalUnit -Name "pinniped-ad" `
|
||||
-ProtectedFromAccidentalDeletion $false
|
||||
|
||||
New-ADOrganizationalUnit -Name "Users" `
|
||||
-Path "OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-ProtectedFromAccidentalDeletion $false
|
||||
|
||||
New-ADOrganizationalUnit -Name "test-users" `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-Description "integration tests will create and delete ephemeral users here" `
|
||||
-ProtectedFromAccidentalDeletion $false
|
||||
|
||||
# Print all OUs to validate that they were created.
|
||||
Get-ADOrganizationalUnit -Filter *
|
||||
|
||||
New-ADUser -Name "Bind User" -SamAccountName "bind-user" -GivenName "Bind" -Surname "User" -DisplayName "Bind User" `
|
||||
-UserPrincipalName "bind-user@activedirectory.test.pinniped.dev" `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-AccountPassword (ConvertTo-SecureString "REDACTED_BIND_USER_PASSWORD" -AsPlainText -Force) `
|
||||
-Enabled $true -PasswordNeverExpires $true
|
||||
|
||||
# Note that the value of EmailAddress is not a real email address, but that's okay.
|
||||
New-ADUser -Name "Pinny Seal" -SamAccountName "pinny" -GivenName "Pinny" -Surname "Seal" -DisplayName "Pinny Seal" `
|
||||
-UserPrincipalName "pinny@activedirectory.test.pinniped.dev" `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-EmailAddress "tanzu-user-authentication@groups.vmware.com" `
|
||||
-AccountPassword (ConvertTo-SecureString "REDACTED_PINNY_USER_PASSWORD" -AsPlainText -Force) `
|
||||
-Enabled $true -PasswordNeverExpires $true
|
||||
|
||||
New-ADUser -Name "Deactivated User" -SamAccountName "deactivated-user" -GivenName "Deactivated" -Surname "User" -DisplayName "Deactivated User" `
|
||||
-UserPrincipalName "deactivated-user@activedirectory.test.pinniped.dev" `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-AccountPassword (ConvertTo-SecureString "REDACTED_DEACTIVATED_USER_PASSWORD" -AsPlainText -Force) `
|
||||
-Enabled $false -PasswordNeverExpires $true
|
||||
|
||||
# Take note of the pinny account's ObjectGUID. You will need to edit the concourse-secrets secret later to update this GUID value.
|
||||
# This value should look something like "288188dd-ab76-4f61-b6e4-c72e081502c5".
|
||||
Get-ADUser pinny -Properties * | Select SamaccountName,ObjectGUID
|
||||
|
||||
# Print all users to validate that they were created.
|
||||
Get-ADUser -Filter *
|
||||
|
||||
New-ADGroup -Name "Marine Mammals" -SamAccountName "Marine Mammals" -DisplayName "Marine Mammals" `
|
||||
-GroupCategory Security -GroupScope Global `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev"
|
||||
|
||||
Add-ADGroupMember -Identity "Marine Mammals" -Members "pinny"
|
||||
|
||||
New-ADGroup -Name "Mammals" -SamAccountName "Mammals" -DisplayName "Mammals" `
|
||||
-GroupCategory Security -GroupScope Global `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev"
|
||||
|
||||
Add-ADGroupMember -Identity "Mammals" -Members "Marine Mammals"
|
||||
|
||||
# Change the default password policy. There are some integration tests that rely on this.
|
||||
# This is the equivalent of doing this in the Windows "Active Directory Administrative Center" UI:
|
||||
# check "enforce account lockout policy", give it 20 failed attempts and a 15-minute reset, then
|
||||
# uncheck "enforce minimum password age" so we can change the password immediately upon creating a user.
|
||||
Set-ADDefaultDomainPasswordPolicy -Identity "activedirectory.test.pinniped.dev" `
|
||||
-LockoutThreshold 20 -LockoutDuration "00:15:00" -LockoutObservationWindow "00:15:00" `
|
||||
-MinPasswordAge 0
|
||||
|
||||
# Print the policy to validate that it was updated.
|
||||
Get-ADDefaultDomainPasswordPolicy
|
||||
|
||||
# We need to allow the bind-user to create/delete/edit users and groups within the test-users OU, because several
|
||||
# integration tests want to crate/delete/edit ephemeral test users and groups.
|
||||
# These access control steps were inspired by https://the-itguy.de/delegate-access-in-active-directory-with-powershell/.
|
||||
# This is intended to be the equivalent of using the UI to assign permissions like this: right click on "test-users",
|
||||
# select Delegate Control, select "bind-user" as the user, select "create, delete and manage user accounts" and
|
||||
# "reset user passwords" as the tasks to delegate.
|
||||
function New-ADDGuidMap
|
||||
{
|
||||
$rootdse = Get-ADRootDSE
|
||||
$guidmap = @{ }
|
||||
$GuidMapParams = @{
|
||||
SearchBase = ($rootdse.SchemaNamingContext)
|
||||
LDAPFilter = "(schemaidguid=*)"
|
||||
Properties = ("lDAPDisplayName", "schemaIDGUID")
|
||||
}
|
||||
Get-ADObject @GuidMapParams | ForEach-Object { $guidmap[$_.lDAPDisplayName] = [System.GUID]$_.schemaIDGUID }
|
||||
return $guidmap
|
||||
}
|
||||
$GuidMap = New-ADDGuidMap
|
||||
$BindUserSID = New-Object System.Security.Principal.SecurityIdentifier (Get-ADUser "bind-user").SID
|
||||
$acl = Get-Acl -Path "AD:OU=test-users,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev"
|
||||
$ace1 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $BindUserSID, "GenericAll", "Allow", "Descendents", $GuidMap["user"]
|
||||
$ace2 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $BindUserSID, "CreateChild, DeleteChild", "Allow", $GuidMap["user"], "All"
|
||||
$ace3 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $BindUserSID, "GenericAll", "Allow", "Descendents", $GuidMap["group"]
|
||||
$ace4 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $BindUserSID, "CreateChild, DeleteChild", "Allow", $GuidMap["group"], "All"
|
||||
$acl.AddAccessRule($ace1)
|
||||
$acl.AddAccessRule($ace2)
|
||||
$acl.AddAccessRule($ace3)
|
||||
$acl.AddAccessRule($ace4)
|
||||
Set-Acl -Path "AD:OU=test-users,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" -AclObject $acl
|
||||
|
||||
# Print the access control rules that were just applied.
|
||||
$acl = Get-Acl -Path "AD:OU=test-users,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev"
|
||||
$acl.Access | Where-Object { $_.IdentityReference -eq "pinniped-ad\bind-user" }
|
||||
```
|
||||
|
||||
If you would like to see these OUs, users, and groups in the UI, you can open the "Active Directory Users and Computers"
|
||||
app in your RDP session.
|
||||
|
||||
## Configure a CA and a serving certificate for LDAPS
|
||||
|
||||
Now we need to create and configure a TLS serving certificate for LDAPS.
|
||||
|
||||
The certificate needs to include two hostnames. One of the hostnames is the name that the AD server
|
||||
thinks is its own hostname (`active-directory-dc1.activedirectory.test.pinniped.dev`).
|
||||
This is how the AD server will decide to use this cert for the LDAPS port.
|
||||
The other hostname is the one that clients will use when making connections from the outside
|
||||
(`activedirectory.test.pinniped.dev`) so they can validate the server certificate.
|
||||
|
||||
The steps here were inspired by https://gist.github.com/magnetikonline/0ccdabfec58eb1929c997d22e7341e45.
|
||||
|
||||
On your mac:
|
||||
|
||||
```shell
|
||||
# On your Mac: Create a self-signed CA public/private keypair.
|
||||
openssl req -x509 -newkey rsa:4096 \
|
||||
-keyout ad-ca.key -out ad-ca.crt \
|
||||
-sha256 -days 36500 -nodes \
|
||||
-subj "/C=US/ST=California/L=San Francisco/O=Pinniped/OU=Pinniped CI/CN=Pinniped AD CA"
|
||||
|
||||
# Copy the public key to your clipboard.
|
||||
cat ad-ca.crt| pbcopy
|
||||
```
|
||||
|
||||
In Powershell terminal:
|
||||
|
||||
```shell
|
||||
# In your Windows RDP session's Powershell terminal, put the content of the clipboard into a file.
|
||||
# Note that if you copy/paste this command to your RDP session, then you need to pbcopy the public
|
||||
# key again before you hit return for this command.
|
||||
Get-Clipboard | Out-File -FilePath "C:\users\administrator\desktop\ca.crt"
|
||||
|
||||
# In Powershell terminal, check that the file exists and looks correct.
|
||||
type "C:\users\administrator\desktop\ca.crt"
|
||||
|
||||
# Import root certificate into trusted store of domain controller in your Powershell terminal:
|
||||
Import-Certificate -FilePath "C:\users\administrator\desktop\ca.crt" -CertStoreLocation Cert:\LocalMachine\Root
|
||||
```
|
||||
|
||||
If you want to validate that this was imported, open the UI tool called "Manage computer certificates"
|
||||
and look in the folder called "Trusted Root Certification Authorities\Certificates".
|
||||
If the UI was already open, click the refresh button.
|
||||
|
||||
Copy the following file contents to your clipboard:
|
||||
|
||||
```shell
|
||||
[Version]
|
||||
Signature="$Windows NT$"
|
||||
|
||||
[NewRequest]
|
||||
Subject = "CN=activedirectory.test.pinniped.dev"
|
||||
KeySpec = 1
|
||||
KeyLength = 2048
|
||||
Exportable = TRUE
|
||||
MachineKeySet = TRUE
|
||||
SMIME = FALSE
|
||||
PrivateKeyArchive = FALSE
|
||||
UserProtected = FALSE
|
||||
UseExistingKeySet = FALSE
|
||||
ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
||||
ProviderType = 12
|
||||
RequestType = PKCS10
|
||||
KeyUsage = 0xa0
|
||||
|
||||
[EnhancedKeyUsageExtension]
|
||||
OID = 1.3.6.1.5.5.7.3.1 ; Server Authentication
|
||||
|
||||
[Extensions]
|
||||
2.5.29.17 = "{text}"
|
||||
_continue_ = "DNS=activedirectory.test.pinniped.dev"
|
||||
_continue_ = "DNS=active-directory-dc1.activedirectory.test.pinniped.dev"
|
||||
```
|
||||
|
||||
In Powershell terminal:
|
||||
|
||||
```shell
|
||||
# In your Windows RDP session's Powershell terminal, put the content of the clipboard into a file.
|
||||
# Note that if you copy/paste this command to your RDP session, then you need to copy the file contents
|
||||
# from above again before you hit return for this command.
|
||||
Get-Clipboard | Out-File -FilePath "C:\users\administrator\desktop\request.inf"
|
||||
|
||||
# In Powershell terminal, check that the file exists and looks correct.
|
||||
type "C:\users\administrator\desktop\request.inf"
|
||||
|
||||
# Create a CSR. This command will also generate a private key for the AD server and save it.
|
||||
certreq -new "C:\users\administrator\desktop\request.inf" "C:\users\administrator\desktop\client.csr"
|
||||
|
||||
# Show the CSR.
|
||||
type "C:\users\administrator\desktop\client.csr"
|
||||
|
||||
# Copy the content of this file to your clipboard.
|
||||
Get-Content "C:\users\administrator\desktop\client.csr" | Set-Clipboard
|
||||
```
|
||||
|
||||
On your mac:
|
||||
|
||||
```shell
|
||||
# On your Mac, use the CA to issue a serving cert based on the CSR.
|
||||
pbpaste > client.csr
|
||||
|
||||
cat <<EOF > v3ext.txt
|
||||
keyUsage=digitalSignature,keyEncipherment
|
||||
extendedKeyUsage=serverAuth
|
||||
subjectKeyIdentifier=hash
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
DNS.1 = activedirectory.test.pinniped.dev
|
||||
DNS.2 = active-directory-dc1.activedirectory.test.pinniped.dev
|
||||
EOF
|
||||
|
||||
# Create a cert from the CSR signed by the CA.
|
||||
openssl x509 \
|
||||
-req -days 36500 \
|
||||
-in client.csr -CA ad-ca.crt -CAkey ad-ca.key -extfile v3ext.txt \
|
||||
-set_serial 01 -out client.crt
|
||||
|
||||
# Inspect the generated certificate.
|
||||
# Ensure the following X509v3 extensions are all present:
|
||||
# Key Usage: Digital Signature, Key Encipherment
|
||||
# Extended Key Usage: TLS Web Server Authentication
|
||||
# Subject Key Identifier
|
||||
# Subject Alternative Name with 2 DNS hostnames
|
||||
# Authority Key Identifier
|
||||
openssl x509 -in client.crt -text
|
||||
|
||||
# Copy the generated cert.
|
||||
cat client.crt | pbcopy
|
||||
```
|
||||
|
||||
In Powershell terminal:
|
||||
|
||||
```shell
|
||||
# In your Windows RDP session's Powershell terminal, put the content of the clipboard into a file.
|
||||
# Note that if you copy/paste this command to your RDP session, then you need to pbcopy the file contents
|
||||
# from above again before you hit return for this command.
|
||||
Get-Clipboard | Out-File -FilePath "C:\users\administrator\desktop\client.crt"
|
||||
|
||||
# In Powershell terminal, check that the file exists and looks correct.
|
||||
type "C:\users\administrator\desktop\client.crt"
|
||||
|
||||
# Add the serving certificate to Windows. This will also automatically associate it to the private key that you
|
||||
# generated with the previous usage of certreq.
|
||||
certreq -accept "C:\users\administrator\desktop\client.crt"
|
||||
|
||||
# If you want to validate that this was imported, open the UI tool called "Manage computer certificates"
|
||||
# and look in the folder called "Personal\Certificates". If the UI was already open, click the refresh button.
|
||||
# Double click on the cert. Ensure that it says, "you have a private key that corresponds to this certificate".
|
||||
# Next, we need to reboot the VM for the cert to get picked up and used for serving incoming LDAPS connections.
|
||||
# After showing you a warning dialog box, this should terminate your RDP session and stop the VM.
|
||||
shutdown /s
|
||||
```
|
||||
|
||||
Wait for the VM to stop, then start the VM again from your Mac:
|
||||
|
||||
```shell
|
||||
gcloud compute instances start active-directory-dc1 --project ${project} --zone ${zone}
|
||||
```
|
||||
|
||||
Wait for the VM to finish booting. Then we can confirm that LDAPS is working. On your Mac:
|
||||
|
||||
```shell
|
||||
# Check that serving cert is being returned on the LDAPS port. This command should show the cert chain.
|
||||
# It should also verify the server cert using our CA. The output should include "Verify return code: 0 (ok)".
|
||||
openssl s_client -connect activedirectory.test.pinniped.dev:636 -showcerts -CAfile ad-ca.crt < /dev/null
|
||||
|
||||
# Unfortunately, the ldapsearch command that comes pre-installed on MacOS does not seem to respect
|
||||
# the LDAPTLS_CACERT env variable. So it will not be able to validate the server certificates.
|
||||
# As a workaround, we can use docker to run ldapsearch commands in a linux container.
|
||||
|
||||
# Test the regular LDAP port by issuing a query on your Mac. The -ZZ option asks it to use StartTLS.
|
||||
# This should list all users. Replace REDACTED_BIND_USER_PASSWORD with the real password.
|
||||
docker run -v "$(pwd):/certs" -e LDAPTLS_CACERT="/certs/ad-ca.crt" --rm -it bitnami/openldap \
|
||||
ldapsearch -d8 -v -x -ZZ -H 'ldap://activedirectory.test.pinniped.dev' \
|
||||
-D 'CN=Bind User,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev' \
|
||||
-w 'REDACTED_BIND_USER_PASSWORD' \
|
||||
-b 'OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev' \
|
||||
-s sub \
|
||||
'(objectClass=user)' '*'
|
||||
|
||||
# Test the LDAPS port by issuing a query on your Mac. This should list all users.
|
||||
# Replace REDACTED_BIND_USER_PASSWORD with the real password.
|
||||
docker run -v "$(pwd):/certs" -e LDAPTLS_CACERT="/certs/ad-ca.crt" --rm -it bitnami/openldap \
|
||||
ldapsearch -d8 -v -x -H 'ldaps://activedirectory.test.pinniped.dev' \
|
||||
-D 'CN=Bind User,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev' \
|
||||
-w 'REDACTED_BIND_USER_PASSWORD' \
|
||||
-b 'OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev' \
|
||||
-s sub \
|
||||
'(objectClass=user)' '*'
|
||||
```
|
||||
|
||||
## Update the `concourse-secrets` secret in GCP Secrets Manager
|
||||
|
||||
On your Mac:
|
||||
|
||||
```shell
|
||||
# Copy the CA's public cert.
|
||||
cat ad-ca.crt | base64 | pbcopy
|
||||
|
||||
# cd to your local clone of the `ci` branch of the pinniped repo
|
||||
cd pinniped-ci-branch
|
||||
|
||||
# Edit the secret.
|
||||
./hack/edit-gcloud-secret.sh concourse-secret
|
||||
# This opens vim to edit the secret.
|
||||
# Paste the cert as the value for `aws-ad-ca-data`.
|
||||
# Also edit the the value of `aws-ad-user-unique-id-attribute-value`. The value should be the ObjectGUID of the pinny
|
||||
# user that you created in the steps above.
|
||||
# Save your changes, exit vim, and when prompted say that you want to save this as the new version of concourse-secrets.
|
||||
```
|
||||
|
||||
## Confirm that Active Directory integration tests can pass
|
||||
|
||||
Use these commands run all the Active Directory integration tests on your Mac.
|
||||
The `-run` filter is based on the tests as they existed at the time of writing this doc.
|
||||
You can find AD tests by searching for `SkipTestWhenActiveDirectoryIsUnavailable`.
|
||||
|
||||
On your Mac:
|
||||
|
||||
```shell
|
||||
# Login so we can read the secrets from GCP Secret Manager.
|
||||
gcloud auth login
|
||||
|
||||
# cd to your local git clone
|
||||
cd pinniped
|
||||
|
||||
# Compile and install onto a local kind cluster.
|
||||
./hack/prepare-for-integration-tests.sh -c --get-active-directory-vars "../pinniped-ci-branch/hack/get-aws-ad-env-vars.sh"
|
||||
|
||||
# Run all the tests that depend on AD.
|
||||
source /tmp/integration-test-env && go test -v -race -count 1 -timeout 0 ./test/integration \
|
||||
-run "/TestSupervisorLogin_Browser/active_directory|/TestE2EFullIntegration_Browser/with_Supervisor_ActiveDirectory|/TestActiveDirectoryIDPPhaseAndConditions_Parallel|/TestSupervisorWarnings_Browser/Active_Directory"
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
On your Mac:
|
||||
|
||||
```shell
|
||||
# Remove all bindings for the service account from the secret.
|
||||
# The binding was only needed during the first boot of the VM.
|
||||
gcloud secrets remove-iam-policy-binding active-directory-dc1-password \
|
||||
--project ${project} \
|
||||
--member "serviceAccount:${dcServiceAccount}" --role roles/secretmanager.secretAccessor \
|
||||
--all
|
||||
|
||||
# Remove the firewall rule which allows incoming RDP connections.
|
||||
# If you need to RDP to this AD VM in the future, then you will need to create
|
||||
# a new firewall rule to allow it.
|
||||
gcloud compute firewall-rules delete allow-rdp-ingress-to-addc \
|
||||
--project ${project} \
|
||||
--quiet
|
||||
|
||||
# Remove all temp files. It's okay to remove the private key for our CA because we
|
||||
# created certs that are good for 100 years, as long as you have already added the
|
||||
# public cert to the concourse-secrets secret. If we need to create a new AD VM, we
|
||||
# can also create a new CA.
|
||||
rm ad-ca.crt ad-ca.key client.crt client.csr v3ext.txt
|
||||
```
|
||||
34
ADOPTERS.md
34
ADOPTERS.md
@@ -1,34 +0,0 @@
|
||||
# Pinniped Adopters
|
||||
|
||||
If you're using Pinniped and want to add your organization to this
|
||||
list, [follow these directions](#adding-your-organization-to-the-list-of-adopters)!
|
||||
|
||||
## Organizations using Pinniped
|
||||
|
||||
<a href="https://tanzu.vmware.com/tanzu" border="0" target="_blank"><img alt="vmware-tanzu" src="site/themes/pinniped/static/img/vmware-tanzu.svg" height="50"></a>
|
||||
|
||||
<a href="https://kubeapps.com/" border="0" target="_blank"><img alt="kubeapps" src="site/themes/pinniped/static/img/kubeapps.svg" height="50"></a>
|
||||
|
||||
<a href="https://www.ok.dk/" border="0" target="_blank"><img alt="ok-amba" src="site/themes/pinniped/static/img/ok-amba.svg" height="50"></a>
|
||||
|
||||
## Solutions built with Pinniped
|
||||
|
||||
Below is a list of solutions where Pinniped is being used as a component.
|
||||
|
||||
**[Kubeapps](https://kubeapps.com/)**
|
||||
|
||||
Kubeapps uses Pinniped to [enable SSO authentication](https://github.com/kubeapps/kubeapps/blob/master/docs/user/using-an-OIDC-provider-with-pinniped.md) when running on clusters where SSO cannot be configured for the cluster API server.
|
||||
|
||||
**[VMware Tanzu Kubernetes Grid (TKG)](https://tanzu.vmware.com/kubernetes-grid)**
|
||||
|
||||
TKG uses Pinniped to provide a seamless SSO experience across management and workload clusters.
|
||||
|
||||
**[VMware Tanzu Mission Control (TMC)](https://tanzu.vmware.com/mission-control)**
|
||||
|
||||
TMC uses Pinniped to provide a uniform authentication experience across all attached clusters.
|
||||
|
||||
## Adding your organization to the list of adopters
|
||||
|
||||
If you are using Pinniped and would like to be included in the list of Pinniped Adopters, add an SVG version of your logo that is less than 150 KB to
|
||||
the [img directory](https://github.com/vmware/pinniped/tree/main/site/themes/pinniped/static/img) in this repo and submit a pull request with your change including 1-2 sentences describing how your organization is using Pinniped. Name the image file something that
|
||||
reflects your company (e.g., if your company is called Acme, name the image acme.svg). Please feel free to send us a message in [#pinniped](https://kubernetes.slack.com/archives/C01BW364RJA) with any questions you may have.
|
||||
@@ -1,84 +1 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [oss-coc@vmware.com](mailto:oss-coc@vmware.com). All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
||||
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
||||
Please see https://github.com/vmware-tanzu/pinniped/blob/main/CODE_OF_CONDUCT.md
|
||||
|
||||
217
CONTRIBUTING.md
217
CONTRIBUTING.md
@@ -1,216 +1 @@
|
||||
# Contributing to Pinniped
|
||||
|
||||
Pinniped is better because of our contributors and [maintainers](MAINTAINERS.md). It is because of you that we can bring
|
||||
great software to the community.
|
||||
|
||||
Contributions to Pinniped are welcome. Here are some things to help you get started.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please see the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
|
||||
## Project Scope
|
||||
|
||||
See [SCOPE.md](./SCOPE.md) for some guidelines about what we consider in and out of scope for Pinniped.
|
||||
|
||||
## Roadmap
|
||||
|
||||
The near-term and mid-term roadmap for the work planned for the project [maintainers](MAINTAINERS.md) is documented in [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
## Discussion
|
||||
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out
|
||||
via GitHub [Discussions](https://github.com/vmware/pinniped/discussions),
|
||||
GitHub [Issues](https://github.com/vmware/pinniped/issues),
|
||||
or in the Kubernetes Slack Workspace within the [#pinniped channel](https://go.pinniped.dev/community/slack).
|
||||
Join our [Google Group](https://go.pinniped.dev/community/group) to receive updates and meeting invitations.
|
||||
|
||||
## Issues
|
||||
|
||||
Need an idea for a project to get started contributing? Take a look at the open
|
||||
[issues](https://github.com/vmware/pinniped/issues).
|
||||
Also check to see if any open issues are labeled with
|
||||
["good first issue"](https://github.com/vmware/pinniped/labels/good%20first%20issue)
|
||||
or ["help wanted"](https://github.com/vmware/pinniped/labels/help%20wanted).
|
||||
|
||||
### Bugs
|
||||
|
||||
To file a bug report, please first open an
|
||||
[issue](https://github.com/vmware/pinniped/issues/new?template=bug_report.md). The project team
|
||||
will work with you on your bug report.
|
||||
|
||||
Once the bug has been validated, a [pull request](https://github.com/vmware/pinniped/compare)
|
||||
can be opened to fix the bug.
|
||||
|
||||
For specifics on what to include in your bug report, please follow the
|
||||
guidelines in the issue and pull request templates.
|
||||
|
||||
### Features
|
||||
|
||||
To suggest a feature, please first open an
|
||||
[issue](https://github.com/vmware/pinniped/issues/new?template=feature-proposal.md)
|
||||
and tag it with `proposal`, or create a new [Discussion](https://github.com/vmware/pinniped/discussions).
|
||||
The project [maintainers](MAINTAINERS.md) will work with you on your feature request.
|
||||
|
||||
Once the feature request has been validated, a [pull request](https://github.com/vmware/pinniped/compare)
|
||||
can be opened to implement the feature.
|
||||
|
||||
For specifics on what to include in your feature request, please follow the
|
||||
guidelines in the issue and pull request templates.
|
||||
|
||||
### Reporting security vulnerabilities
|
||||
|
||||
Please follow the procedure described in [SECURITY.md](SECURITY.md).
|
||||
|
||||
## CLA
|
||||
|
||||
We welcome contributions from everyone, but we can only accept them if you sign
|
||||
our Contributor License Agreement (CLA). If you would like to contribute and you
|
||||
have not signed it, our CLA-bot will walk you through the process when you open
|
||||
a Pull Request. For questions about the CLA process, see the
|
||||
[FAQ](https://cla.vmware.com/faq) or submit a question through the GitHub issue
|
||||
tracker.
|
||||
|
||||
## Learning about Pinniped
|
||||
|
||||
New to Pinniped?
|
||||
- Start here to learn how to install and use Pinniped: [Learn to use Pinniped for federated authentication to Kubernetes clusters](https://pinniped.dev/docs/tutorials/concierge-and-supervisor-demo/)
|
||||
- Start here to learn how to navigate the source code: [Code Walk-through](https://pinniped.dev/docs/reference/code-walkthrough/)
|
||||
- Other more detailed documentation can be found at: [Pinniped Docs](https://pinniped.dev/docs/)
|
||||
|
||||
## Building
|
||||
|
||||
The [Dockerfile](Dockerfile) at the root of the repo can be used to build and
|
||||
package the server-side code. After making a change to the code, rebuild the
|
||||
docker image with the following command.
|
||||
|
||||
```bash
|
||||
# From the root directory of the repo...
|
||||
docker build .
|
||||
```
|
||||
|
||||
The Pinniped CLI client can be built for local use with the following command.
|
||||
|
||||
```bash
|
||||
# From the root directory of the repo...
|
||||
go build -o pinniped ./cmd/pinniped
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Lint
|
||||
|
||||
```bash
|
||||
./hack/module.sh lint
|
||||
```
|
||||
|
||||
### Running Unit Tests
|
||||
|
||||
```bash
|
||||
./hack/module.sh units
|
||||
```
|
||||
|
||||
### Running Integration Tests
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
- [`docker`](https://www.docker.com/)
|
||||
- `htpasswd` (installed by default on MacOS, usually found in `apache2-utils` package for linux)
|
||||
- [`kapp`](https://carvel.dev/#getting-started)
|
||||
- [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start)
|
||||
- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
- [`ytt`](https://carvel.dev/#getting-started)
|
||||
- [`nmap`](https://nmap.org/download.html)
|
||||
- [`openssl`](https://www.openssl.org) (installed by default on MacOS)
|
||||
- [Chrome](https://www.google.com/chrome/)
|
||||
|
||||
On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
|
||||
|
||||
```bash
|
||||
brew install kind carvel-dev/carvel/ytt carvel-dev/carvel/kapp kubectl nmap && brew cask install docker
|
||||
```
|
||||
|
||||
1. Create a kind cluster, compile, create container images, and install Pinniped and supporting test dependencies using:
|
||||
|
||||
```bash
|
||||
./hack/prepare-for-integration-tests.sh
|
||||
```
|
||||
|
||||
1. Run the Pinniped integration tests:
|
||||
|
||||
```bash
|
||||
ulimit -n 512 && source /tmp/integration-test-env && go test -v -count 1 -timeout 0 ./test/integration
|
||||
```
|
||||
|
||||
To run specific integration tests, add the `-run` flag to the above command to specify a regexp for the test names.
|
||||
Use a leading `/` on the regexp because the Pinniped integration tests are automatically nested under several parent tests
|
||||
(see [integration/main_test.go](https://github.com/vmware/pinniped/blob/main/test/integration/main_test.go)).
|
||||
For example, to run an integration test called `TestE2E`, add `-run /TestE2E` to the command shown above.
|
||||
|
||||
1. After making production code changes, recompile, redeploy, and run tests again by repeating the same
|
||||
commands described above. If there are only test code changes, then simply run the tests again.
|
||||
|
||||
To destroy the local Kubernetes cluster, run `./hack/kind-down.sh`.
|
||||
|
||||
#### Using GoLand to Run an Integration Test
|
||||
|
||||
It can sometimes be convenient to use GoLand to run an integration test. For example, this allows using the
|
||||
GoLand debugger to debug the test itself (not the server, since that it running in-cluster).
|
||||
|
||||
Note that the output of `hack/prepare-for-integration-tests.sh` says:
|
||||
|
||||
```bash
|
||||
# Using GoLand? Paste the result of this command into GoLand's run configuration "Environment".
|
||||
# hack/integration-test-env-goland.sh | pbcopy
|
||||
```
|
||||
|
||||
After using `hack/prepare-for-integration-tests.sh`, run `hack/integration-test-env-goland.sh | pbcopy` as instructed. Then:
|
||||
|
||||
1. Select and run an integration test within GoLand. It will fail complaining about missing env vars.
|
||||
1. Pull down the menu that shows the name of the test which you just ran in the previous step, and choose "Edit Configurations...".
|
||||
1. In the "Environment" text box for the run configuration of the integration test that you just ran,
|
||||
paste the results of `hack/integration-test-env-goland.sh | pbcopy`.
|
||||
1. Apply, and then run the integration test again. This time the test will use the environment variables provided.
|
||||
|
||||
Note that if you run `hack/prepare-for-integration-tests.sh` again, then you may need to repeat these steps.
|
||||
Each run of `hack/prepare-for-integration-tests.sh` can result in different values for some of the env vars.
|
||||
|
||||
### Observing Tests on the Continuous Integration Environment
|
||||
|
||||
CI will not be triggered on a pull request until the pull request is reviewed and
|
||||
approved for CI by a project [maintainer](MAINTAINERS.md). Once CI is triggered,
|
||||
the progress and results will appear on the Github page for that
|
||||
[pull request](https://github.com/vmware/pinniped/pulls) as checks. Links
|
||||
will appear to view the details of each check.
|
||||
|
||||
Starting in mid-2025, Pinniped's CI system is no longer externally visible due to corporate policies.
|
||||
Please contact the maintainers for help with your PR if you encounter any CI failures.
|
||||
They will be happy to share CI logs with you directly for your PR.
|
||||
|
||||
## CI
|
||||
|
||||
Pinniped's CI configuration and code is in the [`ci`](https://github.com/vmware/pinniped/tree/ci)
|
||||
branch of this repo.
|
||||
|
||||
## Documentation
|
||||
|
||||
Any pull request which adds a new feature or changes the behavior of any feature which was previously documented
|
||||
should include updates to the documentation. All documentation lives in this repository. This project aspires to
|
||||
follow the Kubernetes [documentation style guide](https://kubernetes.io/docs/contribute/style/style-guide).
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
This project uses [pre-commit](https://pre-commit.com/) to agree on some conventions about whitespace/file encoding.
|
||||
|
||||
```bash
|
||||
$ brew install pre-commit
|
||||
[...]
|
||||
$ pre-commit install
|
||||
pre-commit installed at .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
## Becoming a Pinniped Maintainer
|
||||
|
||||
Regular contributors who are active in the Pinniped community and who have contributed at least several
|
||||
significant pull requests may be considered for promotion to become a maintainer upon request. Please
|
||||
contact an existing [maintainer](MAINTAINERS.md) if you would like to be considered.
|
||||
Please see https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md
|
||||
|
||||
59
Dockerfile
59
Dockerfile
@@ -1,59 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
ARG BUILD_IMAGE=golang:1.25.5@sha256:6cc2338c038bc20f96ab32848da2b5c0641bb9bb5363f2c33e9b7c8838f9a208
|
||||
ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:2b7c93f6d6648c11f0e80a48558c8f77885eb0445213b8e69a6a0d7c89fc6ae4
|
||||
|
||||
# Prepare to cross-compile by always running the build stage in the build platform, not the target platform.
|
||||
FROM --platform=$BUILDPLATFORM $BUILD_IMAGE AS build-env
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
ARG GOPROXY
|
||||
|
||||
ARG KUBE_GIT_VERSION
|
||||
ENV KUBE_GIT_VERSION=$KUBE_GIT_VERSION
|
||||
|
||||
# These will be set by buildkit automatically, e.g. TARGETOS set to "linux" and TARGETARCH set to "amd64" or "arm64".
|
||||
# Useful for building multi-arch container images.
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
# If provided, must be a comma-separated list of Go build tags.
|
||||
ARG ADDITIONAL_BUILD_TAGS
|
||||
|
||||
# Build the statically linked (CGO_ENABLED=0) binary.
|
||||
# Mount source, build cache, and module cache for performance reasons.
|
||||
# See https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/
|
||||
RUN \
|
||||
--mount=target=. \
|
||||
--mount=type=cache,target=/cache/gocache \
|
||||
--mount=type=cache,target=/cache/gomodcache \
|
||||
export GOCACHE=/cache/gocache GOMODCACHE=/cache/gomodcache CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH && \
|
||||
go build -tags $ADDITIONAL_BUILD_TAGS -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-concierge-kube-cert-agent ./cmd/pinniped-concierge-kube-cert-agent/... && \
|
||||
go build -tags $ADDITIONAL_BUILD_TAGS -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-server ./cmd/pinniped-server/... && \
|
||||
ln -s /usr/local/bin/pinniped-server /usr/local/bin/pinniped-concierge && \
|
||||
ln -s /usr/local/bin/pinniped-server /usr/local/bin/pinniped-supervisor && \
|
||||
ln -s /usr/local/bin/pinniped-server /usr/local/bin/local-user-authenticator
|
||||
|
||||
# Use a distroless runtime image with CA certificates, timezone data, and not much else.
|
||||
# Note that we are not using --platform here, so it will choose the base image for the target platform, not the build platform.
|
||||
# By using "distroless/static" instead of "distroless/static-debianXX" we can float on the latest stable version of debian.
|
||||
# See https://github.com/GoogleContainerTools/distroless#base-operating-system
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
# Copy the server binary from the build-env stage.
|
||||
COPY --from=build-env /usr/local/bin /usr/local/bin
|
||||
|
||||
# Document the default server ports for the various server apps
|
||||
EXPOSE 8443 8444 10250
|
||||
|
||||
# Run as non-root for security posture
|
||||
# Use the same non-root user as https://github.com/GoogleContainerTools/distroless/blob/fc3c4eaceb0518900f886aae90407c43be0a42d9/base/base.bzl#L9
|
||||
# This is a workaround for https://github.com/GoogleContainerTools/distroless/issues/718
|
||||
USER 65532:65532
|
||||
|
||||
# Set the entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/pinniped-server"]
|
||||
@@ -1,68 +0,0 @@
|
||||
# Pinniped Governance
|
||||
|
||||
This document defines the project governance for Pinniped.
|
||||
|
||||
# Overview
|
||||
|
||||
**Pinniped** is committed to building an open, inclusive, productive and self-governing open source community focused on
|
||||
building authentication services for Kubernetes clusters. The community is governed by this document which defines how
|
||||
all members should work together to achieve this goal.
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
The Pinniped community abides by this
|
||||
[code of conduct](https://github.com/vmware/pinniped/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
# Community Roles
|
||||
|
||||
* **Users:** Members that engage with the Pinniped community via any medium (Slack, GitHub, mailing lists, etc.).
|
||||
* **Contributors:** Do regular contributions to the Pinniped project (documentation, code reviews, responding to issues,
|
||||
participating in proposal discussions, contributing code, etc.).
|
||||
* **Maintainers:** Responsible for the overall health and direction of the project. They are the final reviewers of PRs
|
||||
and responsible for Pinniped releases.
|
||||
|
||||
# Maintainers
|
||||
|
||||
New maintainers must be nominated by an existing maintainer and must be elected by a supermajority of existing
|
||||
maintainers. Likewise, maintainers can be removed by a supermajority of the existing maintainers or can resign by
|
||||
notifying one of the maintainers.
|
||||
|
||||
**Note:** If a maintainer leaves their employer they are still considered a maintainer of Pinniped, unless they
|
||||
voluntarily resign. Employment is not taken into consideration when determining maintainer eligibility unless the
|
||||
company itself violates our [Code of Conduct](https://github.com/vmware/pinniped/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
# Decision Making
|
||||
|
||||
Ideally, all project decisions are resolved by consensus. If impossible, any maintainer may call a vote. Unless
|
||||
otherwise specified in this document, any vote will be decided by a supermajority of maintainers.
|
||||
|
||||
## Supermajority
|
||||
|
||||
A supermajority is defined as two-thirds of members in the group. A supermajority of maintainers is required for certain
|
||||
decisions as outlined in this document. A supermajority vote is equivalent to the number of votes in favor being at
|
||||
least twice the number of votes against. A vote to abstain equals not voting at all. For example, if you have 5
|
||||
maintainers who all cast non-abstaining votes, then a supermajority vote is at least 4 votes in favor. Voting on
|
||||
decisions can happen on the mailing list, GitHub, Slack, email, or via a voting service, when appropriate. Maintainers
|
||||
can either vote "agree, yes, +1", "disagree, no, -1", or "abstain". A vote passes when supermajority is met.
|
||||
|
||||
## Lazy Consensus
|
||||
|
||||
To maintain velocity in Pinniped, the concept of [Lazy Consensus](http://en.osswiki.info/concepts/lazy_consensus) is
|
||||
practiced.
|
||||
|
||||
Other maintainers may chime in and request additional time for review, but should remain cognizant of blocking progress
|
||||
and abstain from delaying progress unless absolutely needed. The expectation is that blocking progress is accompanied by
|
||||
a guarantee to review and respond to the relevant action in short order.
|
||||
|
||||
Lazy consensus does not apply to the process of:
|
||||
|
||||
* Removal of maintainers from Pinniped
|
||||
|
||||
## Updating Governance
|
||||
|
||||
All substantive changes in Governance, including substantive changes to the proposal process, require a supermajority
|
||||
agreement by all maintainers.
|
||||
|
||||
# Proposal Process
|
||||
|
||||
The proposal process is defined in [proposals/README.md](proposals/README.md).
|
||||
@@ -1,18 +1 @@
|
||||
# Current Pinniped Maintainers
|
||||
|
||||
| Maintainer | GitHub ID | Affiliation |
|
||||
|-----------------|-----------------------------------------------------------|------------------------------------------|
|
||||
| Ryan Richard | [cfryanr](https://github.com/cfryanr) | [VMware](https://www.github.com/vmware/) |
|
||||
| Joshua T. Casey | [joshuatcasey](https://github.com/joshuatcasey) | [VMware](https://www.github.com/vmware/) |
|
||||
|
||||
## Emeritus Maintainers
|
||||
|
||||
| Maintainer | GitHub ID |
|
||||
|-------------------|-----------------------------------------------------------|
|
||||
| Andrew Keesler | [ankeesler](https://github.com/ankeesler) |
|
||||
| Anjali Telang | [anjaltelang](https://github.com/anjaltelang) |
|
||||
| Ben Petersen | [benjaminapetersen](https://github.com/benjaminapetersen) |
|
||||
| Margo Crawford | [margocrawf](https://github.com/margocrawf) |
|
||||
| Matt Moyer | [mattmoyer](https://github.com/mattmoyer) |
|
||||
| Mo Khan | [enj](https://github.com/enj) |
|
||||
| Pablo Schuhmacher | [pabloschuhmacher](https://github.com/pabloschuhmacher) |
|
||||
Please see https://github.com/vmware-tanzu/pinniped/blob/main/MAINTAINERS.md
|
||||
|
||||
239
README.md
239
README.md
@@ -1,47 +1,216 @@
|
||||
<a href="https://pinniped.dev" target="_blank">
|
||||
<img src="site/content/docs/img/pinniped_logo.svg" alt="Pinniped Logo" width="100%"/>
|
||||
</a>
|
||||
# Pinniped's `ci` branch
|
||||
|
||||
## Overview
|
||||
This `ci` branch contains the CI/CD tooling for [Pinniped](https://github.com/vmware-tanzu/pinniped).
|
||||
|
||||
Pinniped provides identity services to Kubernetes.
|
||||
The documentation and code in this branch is mainly intended for the maintainers of Pinniped.
|
||||
|
||||
- Easily plug in external identity providers into Kubernetes clusters while offering a simple install and configuration experience. Leverage first class integration with Kubernetes and kubectl command-line.
|
||||
- Give users a consistent, unified login experience across all your clusters, including on-premises and managed cloud environments.
|
||||
- Securely integrate with an enterprise IDP using standard protocols or use secure, externally managed identities instead of relying on simple, shared credentials.
|
||||
This branch is not intended to be merged to the `main` branch.
|
||||
|
||||
To learn more, please visit the Pinniped project's website, https://pinniped.dev.
|
||||
The code in the branch previously lived in a private repository. It was made public by moving
|
||||
the code into the `ci` branch of the Pinniped repository in late 2024. The previous git history
|
||||
for these files was not copied from the private repository at the time of this migration.
|
||||
|
||||
## Getting started with Pinniped
|
||||
## Reporting an issue in this branch
|
||||
|
||||
Care to kick the tires? It's easy to [install and try Pinniped](https://pinniped.dev/docs/).
|
||||
|
||||
## Discussion
|
||||
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out
|
||||
via GitHub [Discussions](https://github.com/vmware/pinniped/discussions),
|
||||
GitHub [Issues](https://github.com/vmware/pinniped/issues),
|
||||
or in the Kubernetes Slack Workspace within the [#pinniped channel](https://go.pinniped.dev/community/slack).
|
||||
Join our [Google Group](https://go.pinniped.dev/community/group) to receive updates and meeting invitations.
|
||||
|
||||
## Contributions
|
||||
|
||||
Pinniped is better because of our contributors and [maintainers](MAINTAINERS.md). It is because of you that we can bring
|
||||
great software to the community.
|
||||
|
||||
Want to get involved? Contributions are welcome.
|
||||
|
||||
Please see the [contributing guide](CONTRIBUTING.md) for more information about reporting bugs, requesting features,
|
||||
building and testing the code, submitting PRs, and other contributor topics.
|
||||
|
||||
## Adopters
|
||||
|
||||
Some organizations and products using Pinniped are featured in [ADOPTERS.md](ADOPTERS.md).
|
||||
Add your own organization or product [here](https://github.com/vmware/pinniped/discussions/152).
|
||||
Found a bug or would like to make an enhancement request?
|
||||
Please report issues in [this repo](https://github.com/vmware-tanzu/pinniped).
|
||||
|
||||
## Reporting security vulnerabilities
|
||||
|
||||
Please follow the procedure described in [SECURITY.md](SECURITY.md).
|
||||
Please follow the procedure described in [SECURITY.md](https://github.com/vmware-tanzu/pinniped/blob/main/SECURITY.md).
|
||||
|
||||
## Creating a release
|
||||
|
||||
When the team is preparing to ship a release, a maintainer will create a new
|
||||
GitHub [Issue](https://github.com/vmware-tanzu/pinniped/issues/new/choose) in this repo to
|
||||
collaboratively track progress on the release checklist. As tasks are completed,
|
||||
the team will check them off. When all the tasks are completed, the issue is closed.
|
||||
|
||||
The release checklist is committed to this repo as an [issue template](https://github.com/vmware-tanzu/pinniped/tree/main/.github/ISSUE_TEMPLATE/release_checklist.md).
|
||||
|
||||
## Pipelines
|
||||
|
||||
Pinniped uses [Concourse](https://concourse-ci.org) for CI/CD.
|
||||
We are currently running our Concourse on a network that can only be reached from inside the corporate network at [ci.pinniped.broadcom.net](https://ci.pinniped.broadcom.net).
|
||||
|
||||
The following pipelines are implemented in this branch. Not all pipelines are necessarily publicly visible, although our goal is to make them all visible.
|
||||
|
||||
- `main`
|
||||
|
||||
This is the main pipeline that runs on merges to `main`. It builds, tests, and (when manually triggered) releases from main.
|
||||
|
||||
- `pull-requests`
|
||||
|
||||
This is a pipeline that triggers for each open pull request. It runs a smaller subset of the integration tests and validations as `pinniped`.
|
||||
|
||||
- `dockerfile-builders`
|
||||
|
||||
This pipeline builds a bunch of custom utility container images that are used in our CI and testing.
|
||||
|
||||
- `build-gi-cli` (a container image that includes the GitHub CLI)
|
||||
- `build-github-pr-resource` (a [fork](https://github.com/pinniped-ci-bot/github-pr-resource) of the `github-pr-resource` with support for gating PRs for untrusted users)
|
||||
- `build-code-coverage-uploader` (uploading code coverage during unit tests)
|
||||
- `build-eks-deployer-dockerfile` (deploying our app to EKS clusters)
|
||||
- `build-k8s-app-deployer-dockerfile` (deploying our app to clusters)
|
||||
- `build-pool-trigger-resource-dockerfile` (an updated implementation of the [pool-trigger-resource](https://github.com/cfmobile/pool-trigger-resource) for use in our CI)
|
||||
- `build-integration-test-runner-dockerfile` (running our integration tests)
|
||||
- `build-integration-test-runner-beta-dockerfile` (running our integration tests with the latest Chrome beta version)
|
||||
- `build-deployment-yaml-formatter-dockerfile` (templating our deployment YAML during a release)
|
||||
- `build-crane` (copy and tag container images during release)
|
||||
- `build-k8s-code-generator-*` (running our Kubernetes code generation under different Kubernetes dependency versions)
|
||||
- `build-test-dex` (a Dex used during tests)
|
||||
- `build-test-cfssl` (a cfssl used during tests)
|
||||
- `build-test-kubectl` (a kubectl used during tests)
|
||||
- `build-test-forward-proxy` (a Squid forward proxy used during tests)
|
||||
- `build-test-bitnami-ldap` (an OpenLDAP used during tests)
|
||||
|
||||
- `cleanup-aws`
|
||||
|
||||
This runs a script that runs [aws-nuke](https://github.com/rebuy-de/aws-nuke) against our test AWS account.
|
||||
This was occasionally needed because [eksctl](https://eksctl.io/) sometimes fails and leaks AWS resources. These resources cost money and use up our AWS quota.
|
||||
However, we seem to have worked around these issues and this pipeline has not been used for some time.
|
||||
|
||||
These jobs are only triggered manually. This is dangerous and should be used with care.
|
||||
|
||||
- `concourse-workers`
|
||||
|
||||
Deploys worker replicas on a long-lived GKE cluster that runs the Concourse workers, and can scale them up or down.
|
||||
|
||||
- `go-compatibility`
|
||||
|
||||
This pipeline runs nightly jobs that validate the compatibility of our code as a Go module in various contexts. We have jobs that test that our code compiles under older Go versions and that our CLI can be installed using `go install`.
|
||||
|
||||
- `security-scan`
|
||||
|
||||
This pipeline has nightly jobs that run security scans on our current main branch and most recently released artifacts.
|
||||
|
||||
The tools we use are:
|
||||
- [sonatype-nexus-community/nancy](https://github.com/sonatype-nexus-community/nancy), which scans Go module versions.
|
||||
- [aquasecurity/trivy](https://github.com/aquasecurity/trivy), which scans container images and Go binaries.
|
||||
- [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck), which scans Go code to find calls to known-vulnerable dependencies.
|
||||
|
||||
This pipeline also has a job called `all-golang-deps-updated` which automatically submits PRs to update all
|
||||
direct dependencies in Pinniped's go.mod file, and update the Golang and distroless container images used in
|
||||
Pinniped's Dockerfiles.
|
||||
|
||||
- `kind-node-builder`
|
||||
|
||||
A nightly build job which uses the latest version of kind to build the HEAD of master of Kubernetes as a container
|
||||
image that can be used to deploy kind clusters. Other pipelines use this container image to install Pinniped and run
|
||||
integration tests. This gives us insight in any compatibility problems with the upcoming next release of Kubernetes.
|
||||
|
||||
## Deploying pipeline changes
|
||||
|
||||
After any shared tasks (`./pipelines/shared-tasks`) or helpers (`./pipelines/shared-helpers`) are edited,
|
||||
the commits must be pushed to the `ci` branch of this repository to take effect.
|
||||
|
||||
After editing any CI secrets or pipeline definitions, a maintainer must run the corresponding
|
||||
`./pipelines/$PIPELINE_NAME/update-pipeline.sh` script to apply the changes to Concourse.
|
||||
To deploy _all_ pipelines, a maintainer can run `./pipelines/update-all-pipelines.sh`.
|
||||
Don't forget to commit and push your changes after applying them!
|
||||
|
||||
## Github webhooks for pipelines
|
||||
|
||||
Some pipelines use github [webhooks to trigger resource checks](https://concourse-ci.org/resources.html#schema.resource.webhook_token),
|
||||
rather than the default of polling every minute, to make these pipelines more responsive and use fewer compute resources
|
||||
for running checks. Refer to places where `webhook_token` is configured in various `pipeline.yml` files.
|
||||
|
||||
To make these webhooks work, they must be defined on the [GitHub repo's settings](https://github.com/vmware-tanzu/pinniped/settings/hooks).
|
||||
|
||||
## Installing and operating Concourse
|
||||
|
||||
See [infra/README.md](./infra/README.md) for details about how Concourse was installed and how it can be operated.
|
||||
|
||||
## Acceptance environments
|
||||
|
||||
In addition to the many ephemeral Kubernetes clusters we use for testing, we also deploy a long-running acceptance environment.
|
||||
|
||||
Google Kubernetes Engine (GKE) in the `gke-acceptance-cluster` cluster in our GCP project in the `us-central1-c` availability zone.
|
||||
|
||||
To access this cluster, download the kubeconfig to `gke-acceptance.yaml` by running:
|
||||
|
||||
```cmd
|
||||
KUBECONFIG=gke-acceptance.yaml gcloud container clusters get-credentials gke-acceptance-cluster --project "$PINNIPED_GCP_PROJECT" --zone us-central1-c
|
||||
```
|
||||
|
||||
The above command assumes that you have already set `PINNIPED_GCP_PROJECT` to be the name of the GCP project.
|
||||
|
||||
## CI secrets
|
||||
|
||||
We use [Google Secret Manager](https://cloud.google.com/secret-manager) on GCP to store build/test/release secrets.
|
||||
These secrets are only available to the maintainers.
|
||||
|
||||
Using the `gcloud secrets list` command or the [web console](https://console.cloud.google.com/security/secret-manager),
|
||||
you can list the available secrets. The content of each secret is a YAML file with secret key/value pairs.
|
||||
You can also use the `./hack/edit-gcloud-secret.sh <secretName>` script to edit or inspect each secret.
|
||||
|
||||
## Configure Azure for CI to test on AKS
|
||||
|
||||
There are several CI jobs which test that Pinniped works when installed on Azure's AKS.
|
||||
For these jobs to run, they need to be able to create and delete ephemeral AKS clusters.
|
||||
This requires the following:
|
||||
|
||||
1. An active Azure Subscription. (A "subscription" in Azure is the equivalent of an "account" in AWS or a "project" in GCP.)
|
||||
2. An Azure App Registration (basically, a service account) active in the same Directory (aka tenant) as the Subscription.
|
||||
Create the app in "My Organization Only". It does not need a redirect URI or any other optional settings.
|
||||
Create a client secret for this app. If you want the client secret to have a long lifetime, you can use the `az` CLI to create it.
|
||||
In the Subscription's IAM settings, assign this app the role "Azure Kubernetes Service Contributor Role" to allow
|
||||
the app to manage AKS clusters. Also assign this app the role "Reader" to allow it to read all resources
|
||||
(used by the `remove-orphaned-aks-clusters` CI task).
|
||||
Do not grant this app permissions in any other Subscription or use it for any other purpose.
|
||||
3. Configure the pipelines with the app's Application (client) ID, Client Secret, and Directory (tenant) ID
|
||||
as the appropriate secret values.
|
||||
|
||||
The CI jobs will create and delete AKS clusters in a Resource Group called `pinniped-ci` within the provided Subscription.
|
||||
|
||||
## Configure AWS for CI to test on EKS
|
||||
|
||||
There are several CI jobs which test that Pinniped works when installed on Amazon's EKS.
|
||||
For these jobs to run, they need to be able to create and delete ephemeral EKS clusters.
|
||||
There are also some jobs to cleanup any orphaned resources (e.g. IP addresses) in the AWS account.
|
||||
These jobs requires the following:
|
||||
|
||||
1. An active AWS account, which will only be used for this purpose.
|
||||
2. Two IAM users in that account, each with a role that can be assumed.
|
||||
These IAM users which should only be used for Pinniped CI and no other purpose.
|
||||
They should only have permissions to perform AWS actions in the relevant AWS account, and no other account.
|
||||
3. The first user and role should have permission to create and delete EKS clusters using `eksctl`.
|
||||
The permissions required can be found in the [eksctl docs](https://eksctl.io/usage/minimum-iam-policies).
|
||||
The user also needs permission to run `aws logs put-retention-policy`, `aws ec2 describe-nat-gateways`,
|
||||
and `aws ec2 delete-nat-gateway`.
|
||||
4. The second user and role should have broad permissions to get and delete everything in the account.
|
||||
It will be used to run `aws-nuke` to list and/or clean resources from the AWS account.
|
||||
To use `aws-nuke`, the user also needs to have an AWS account alias
|
||||
(see the [cleanup-aws task](pipelines/shared-tasks/cleanup-aws/task.sh) for details).
|
||||
|
||||
## Setting Up Active Directory Test Environment
|
||||
|
||||
To test the `ActiveDirectoryIdentityProvider` functionality, we have a long-running Active Directory Domain Controller
|
||||
server instance in our GCP account. See [AD-SETUP.md](AD-SETUP.md) for details.
|
||||
|
||||
## Running integration tests on your laptop using AD
|
||||
|
||||
The relevant environment variables can be pulled from the secret manager via the `hack/get-active-directory-env-vars.sh` script.
|
||||
This can be used by maintainers with Pinniped's `/hack/prepare-for-integration-tests.sh` script in the following way:
|
||||
|
||||
```bash
|
||||
# Must authenticate to glcoud to access the secret manager.
|
||||
gcloud auth login
|
||||
# In the pinniped repo's main branch or in your PR branch:
|
||||
hack/prepare-for-integration-tests.sh --get-active-directory-vars "$HOME/path/to/pinniped-ci-branch/hack/get-active-directory-env-vars.sh"
|
||||
```
|
||||
|
||||
## Running integration tests on your laptop using GitHub
|
||||
|
||||
The relevant environment variables can be pulled from the secret manager via the `hack/get-github-env-vars.sh` script.
|
||||
This can be used by maintainers with Pinniped's `/hack/prepare-for-integration-tests.sh` script in the following way:
|
||||
|
||||
```bash
|
||||
# Must authenticate to glcoud to access the secret manager.
|
||||
gcloud auth login
|
||||
# In the pinniped repo's main branch or in your PR branch:
|
||||
hack/prepare-for-integration-tests.sh --get-github-vars "$HOME/path/to/pinniped-ci-branch/hack/get-github-env-vars.sh"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
|
||||
26
ROADMAP.md
26
ROADMAP.md
@@ -1,26 +0,0 @@
|
||||
## Pinniped Project Roadmap
|
||||
|
||||
### About this document
|
||||
|
||||
This document provides a high-level overview of the next big features the maintainers are planning to work on. This
|
||||
should serve as a reference point for Pinniped users and contributors to understand where the project is heading, and
|
||||
help determine if a contribution could be conflicting with a longer term plan.
|
||||
|
||||
### How to help
|
||||
|
||||
Discussion on the roadmap is welcomed. If you want to provide suggestions, use cases, and feedback to an item in the
|
||||
roadmap, please reach out to the maintainers using one of the methods described in the project's
|
||||
[README.md](https://github.com/vmware/pinniped#discussion).
|
||||
[Contributions](https://github.com/vmware/pinniped/blob/main/CONTRIBUTING.md) to Pinniped are also welcomed.
|
||||
|
||||
### How to add an item to the roadmap
|
||||
|
||||
One of the most important aspects in any open source community is the concept of proposals. Large changes to the
|
||||
codebase and / or new features should be preceded by
|
||||
a [proposal](https://github.com/vmware/pinniped/tree/main/proposals) in our repo.
|
||||
For smaller enhancements, you can open an issue to track that initiative or feature request.
|
||||
We work with and rely on community feedback to focus our efforts to improve Pinniped and maintain a healthy roadmap.
|
||||
|
||||
Priorities and requirements change based on community feedback, roadblocks encountered, community contributions,
|
||||
etc. If you depend on a specific item, we encourage you to reach out for updated status information, or help us deliver
|
||||
that feature by [contributing](https://github.com/vmware/pinniped/blob/main/CONTRIBUTING.md) to Pinniped.
|
||||
23
SCOPE.md
23
SCOPE.md
@@ -1,23 +0,0 @@
|
||||
# Project Scope
|
||||
|
||||
The Pinniped project is guided by the following principles.
|
||||
|
||||
- Pinniped lets you plug any external identity providers into Kubernetes.
|
||||
These integrations follow enterprise-grade security principles.
|
||||
- Pinniped is easy to install and use on any Kubernetes cluster via distribution-specific integration mechanisms.
|
||||
- Pinniped uses a declarative configuration via Kubernetes APIs.
|
||||
- Pinniped provides optimal user experience when authenticating to many clusters at one time.
|
||||
- Pinniped provides enterprise-grade security posture via secure defaults and revocable or very short-lived credentials.
|
||||
- Where possible, Pinniped will contribute ideas and code to upstream Kubernetes.
|
||||
|
||||
When contributing to Pinniped, please consider whether your contribution follows
|
||||
these guiding principles.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
The following items are out of scope for the Pinniped project.
|
||||
|
||||
- Authorization.
|
||||
- Standalone identity provider for general use.
|
||||
- Machine-to-machine (service) identity.
|
||||
- Running outside of Kubernetes.
|
||||
93
SECURITY.md
93
SECURITY.md
@@ -1,92 +1 @@
|
||||
# Security Release Process
|
||||
|
||||
Pinniped provides identity services for Kubernetes clusters. The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As of right now, only the latest version of Pinniped is supported.
|
||||
|
||||
## Reporting a Vulnerability - Private Disclosure Process
|
||||
|
||||
Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to Pinniped privately, to minimize attacks against current users of Pinniped before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project.
|
||||
|
||||
If you know of a publicly disclosed security vulnerability for Pinniped, please **IMMEDIATELY** contact the VMware Security Team (vmware.psirt@broadcom.com). The use of encrypted email is encouraged. The public PGP key can be found at https://kb.vmware.com/kb/1055.
|
||||
|
||||
**IMPORTANT: Do not file public issues on GitHub for security vulnerabilities**
|
||||
|
||||
To report a vulnerability or a security-related issue, please contact the VMware email address with the details of the vulnerability. The email will be fielded by the VMware Security Team and then shared with the Pinniped maintainers who have committer and release permissions. Emails will be addressed within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/vmware/pinniped/issues/new/choose) instead.
|
||||
|
||||
## Proposed Email Content
|
||||
|
||||
Provide a descriptive subject line and in the body of the email include the following information:
|
||||
|
||||
* Basic identity information, such as your name and your affiliation or company.
|
||||
* Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and logs are all helpful to us).
|
||||
* Description of the effects of the vulnerability on Pinniped and the related hardware and software configurations, so that the VMware Security Team can reproduce it.
|
||||
* How the vulnerability affects Pinniped usage and an estimation of the attack surface, if there is one.
|
||||
* List other projects or dependencies that were used in conjunction with Pinniped to produce the vulnerability.
|
||||
|
||||
## When to report a vulnerability
|
||||
|
||||
* When you think Pinniped has a potential security vulnerability.
|
||||
* When you suspect a potential vulnerability but you are unsure that it impacts Pinniped.
|
||||
* When you know of or suspect a potential vulnerability on another project that is used by Pinniped.
|
||||
|
||||
## Patch, Release, and Disclosure
|
||||
|
||||
The VMware Security Team will respond to vulnerability reports as follows:
|
||||
|
||||
1. The Security Team will investigate the vulnerability and determine its effects and criticality.
|
||||
2. If the issue is not deemed to be a vulnerability, the Security Team will follow up with a detailed reason for rejection.
|
||||
3. The Security Team will initiate a conversation with the reporter within 3 business days.
|
||||
4. If a vulnerability is acknowledged and the timeline for a fix is determined, the Security Team will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out.
|
||||
5. The Security Team will also create a [CVSS](https://www.first.org/cvss/specification-document) using the [CVSS Calculator](https://www.first.org/cvss/calculator/3.0). The Security Team makes the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect. Issues may also be reported to [Mitre](https://cve.mitre.org/) using this [scoring calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The CVE will initially be set to private.
|
||||
6. The Security Team will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix.
|
||||
7. The Security Team will provide early disclosure of the vulnerability by emailing the [Pinniped Distributors](https://groups.google.com/g/project-pinniped-distributors) mailing list. Distributors can initially plan for the vulnerability patch ahead of the fix, and later can test the fix and provide feedback to the Pinniped team. See the section **Early Disclosure to Pinniped Distributors List** for details about how to join this mailing list.
|
||||
8. A public disclosure date is negotiated by the VMware SecurityTeam, the bug submitter, and the distributors list. We prefer to fully disclose the bug as soon as possible once a user mitigation or patch is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for distributor coordination. The timeframe for disclosure is from immediate (especially if it’s already publicly known) to a few weeks. For a critical vulnerability with a straightforward mitigation, we expect the report date for the public disclosure date to be on the order of 14 business days. The VMware Security Team holds the final say when setting a public disclosure date.
|
||||
9. Once the fix is confirmed, the Security Team will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. Upon release of the patched version of Pinniped, we will follow the **Public Disclosure Process**.
|
||||
|
||||
## Public Disclosure Process
|
||||
|
||||
The Security Team publishes a [public advisory](https://github.com/vmware/pinniped/security/advisories) to the Pinniped community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog and other channels will assist in educating Pinniped users and rolling out the patched release to affected users.
|
||||
|
||||
The Security Team will also publish any mitigating steps users can take until the fix can be applied to their Pinniped instances. Pinniped distributors will handle creating and publishing their own security advisories.
|
||||
|
||||
## Mailing lists
|
||||
|
||||
* Use vmware.psirt@broadcom.com to report security concerns to the VMware Security Team, who uses the list to privately discuss security issues and fixes prior to disclosure. The use of encrypted email is encouraged. The public PGP key can be found at https://kb.vmware.com/kb/1055.
|
||||
* Join the [Pinniped Distributors](https://groups.google.com/g/project-pinniped-distributors) mailing list for early private information and vulnerability disclosure. Early disclosure may include mitigating steps and additional information on security patch releases. See below for information on how Pinniped distributors or vendors can apply to join this list.
|
||||
|
||||
## Early Disclosure to Pinniped Distributors List
|
||||
|
||||
The private list is intended to be used primarily to provide actionable information to multiple distributor projects at once. This list is not intended to inform individuals about security issues.
|
||||
|
||||
## Membership Criteria
|
||||
|
||||
To be eligible to join the [Pinniped Distributors](https://groups.google.com/g/project-pinniped-distributors) mailing list, you should:
|
||||
|
||||
1. Be an active distributor of Pinniped.
|
||||
2. Have a user base that is not limited to your own organization.
|
||||
3. Have a publicly verifiable track record up to the present day of fixing security issues.
|
||||
4. Not be a downstream or rebuild of another distributor.
|
||||
5. Be a participant and active contributor in the Pinniped community.
|
||||
6. Accept the Embargo Policy that is outlined below.
|
||||
7. Have someone who is already on the list vouch for the person requesting membership on behalf of your distribution.
|
||||
|
||||
**The terms and conditions of the Embargo Policy apply to all members of this mailing list. A request for membership represents your acceptance to the terms and conditions of the Embargo Policy.**
|
||||
|
||||
## Embargo Policy
|
||||
|
||||
The information that members receive on the Pinniped Distributors mailing list must not be made public, shared, or even hinted at anywhere beyond those who need to know within your specific team, unless you receive explicit approval to do so from the VMware Security Team. This remains true until the public disclosure date/time agreed upon by the list. Members of the list and others cannot use the information for any reason other than to get the issue fixed for your respective distribution's users.
|
||||
|
||||
Before you share any information from the list with members of your team who are required to fix the issue, these team members must agree to the same terms, and only be provided with information on a need-to-know basis.
|
||||
|
||||
In the unfortunate event that you share information beyond what is permitted by this policy, you must urgently inform the VMware Security Team (vmware.psirt@broadcom.com) of exactly what information was leaked and to whom. If you continue to leak information and break the policy outlined here, you will be permanently removed from the list.
|
||||
|
||||
## Requesting to Join
|
||||
|
||||
Send new membership requests to https://groups.google.com/g/project-pinniped-distributors. In the body of your request please specify how you qualify for membership and fulfill each criterion listed in the Membership Criteria section above.
|
||||
|
||||
## Confidentiality, integrity and availability
|
||||
|
||||
We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. The VMware Security Team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner.
|
||||
Please see https://github.com/vmware-tanzu/pinniped/blob/main/SECURITY.md
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# API Generation Templates
|
||||
|
||||
This directory contains a template for generating our Kubernetes API code across several Kubernetes versions.
|
||||
|
||||
See the [`./generated`](../generated) directory for the rendered output.
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=authentication.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authentication API.
|
||||
package v1alpha1
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "authentication.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WebhookAuthenticator{},
|
||||
&WebhookAuthenticatorList{},
|
||||
&JWTAuthenticator{},
|
||||
&JWTAuthenticatorList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// JWTAuthenticatorStatus is the status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// JWTAuthenticatorSpec is the spec for configuring a JWT authenticator.
|
||||
type JWTAuthenticatorSpec struct {
|
||||
// issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is
|
||||
// also used to validate the "iss" JWT claim.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://`
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// audience is the required value of the "aud" JWT claim.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Audience string `json:"audience"`
|
||||
|
||||
// claims allows customization of the claims that will be mapped to user identity
|
||||
// for Kubernetes access.
|
||||
// +optional
|
||||
Claims JWTTokenClaims `json:"claims"`
|
||||
|
||||
// claimValidationRules are rules that are applied to validate token claims to authenticate users.
|
||||
// This is similar to claimValidationRules from Kubernetes AuthenticationConfiguration as documented in
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication.
|
||||
// This is an advanced configuration option. During an end-user login flow, mistakes in this
|
||||
// configuration will cause the user's login to fail.
|
||||
// +optional
|
||||
ClaimValidationRules []ClaimValidationRule `json:"claimValidationRules,omitempty"`
|
||||
|
||||
// userValidationRules are rules that are applied to final user before completing authentication.
|
||||
// These allow invariants to be applied to incoming identities such as preventing the
|
||||
// use of the system: prefix that is commonly used by Kubernetes components.
|
||||
// The validation rules are logically ANDed together and must all return true for the validation to pass.
|
||||
// This is similar to claimValidationRules from Kubernetes AuthenticationConfiguration as documented in
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication.
|
||||
// This is an advanced configuration option. During an end-user login flow, mistakes in this
|
||||
// configuration will cause the user's login to fail.
|
||||
// +optional
|
||||
UserValidationRules []UserValidationRule `json:"userValidationRules,omitempty"`
|
||||
|
||||
// tls is the configuration for communicating with the OIDC provider via TLS.
|
||||
// +optional
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// ClaimValidationRule provides the configuration for a single claim validation rule.
|
||||
type ClaimValidationRule struct {
|
||||
// claim is the name of a required claim.
|
||||
// Only string claim keys are supported.
|
||||
// Mutually exclusive with expression and message.
|
||||
// +optional
|
||||
Claim string `json:"claim,omitempty"`
|
||||
|
||||
// requiredValue is the value of a required claim.
|
||||
// Only string claim values are supported.
|
||||
// If claim is set and requiredValue is not set, the claim must be present with a value set to the empty string.
|
||||
// Mutually exclusive with expression and message.
|
||||
// +optional
|
||||
RequiredValue string `json:"requiredValue,omitempty"`
|
||||
|
||||
// expression represents the expression which will be evaluated by CEL.
|
||||
// Must produce a boolean.
|
||||
//
|
||||
// CEL expressions have access to the contents of the token claims, organized into CEL variable:
|
||||
// - 'claims' is a map of claim names to claim values.
|
||||
// For example, a variable named 'sub' can be accessed as 'claims.sub'.
|
||||
// Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'.
|
||||
// Must return true for the validation to pass.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// Mutually exclusive with claim and requiredValue.
|
||||
// +optional
|
||||
Expression string `json:"expression,omitempty"`
|
||||
|
||||
// message customizes the returned error message when expression returns false.
|
||||
// message is a literal string.
|
||||
// Mutually exclusive with claim and requiredValue.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// UserValidationRule provides the configuration for a single user info validation rule.
|
||||
type UserValidationRule struct {
|
||||
// expression represents the expression which will be evaluated by CEL.
|
||||
// Must return true for the validation to pass.
|
||||
//
|
||||
// CEL expressions have access to the contents of UserInfo, organized into CEL variable:
|
||||
// - 'user' - authentication.k8s.io/v1, Kind=UserInfo object
|
||||
// Refer to https://github.com/kubernetes/api/blob/release-1.28/authentication/v1/types.go#L105-L122 for the definition.
|
||||
// API documentation: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#userinfo-v1-authentication-k8s-io
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// +required
|
||||
Expression string `json:"expression"`
|
||||
|
||||
// message customizes the returned error message when rule returns false.
|
||||
// message is a literal string.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// JWTTokenClaims allows customization of the claims that will be mapped to user identity
|
||||
// for Kubernetes access.
|
||||
type JWTTokenClaims struct {
|
||||
// username is the name of the claim which should be read to extract the
|
||||
// username from the JWT token. When not specified, it will default to "username",
|
||||
// unless usernameExpression is specified.
|
||||
//
|
||||
// Mutually exclusive with usernameExpression. Use either username or usernameExpression to
|
||||
// determine the user's username from the JWT token.
|
||||
// +optional
|
||||
Username string `json:"username"`
|
||||
|
||||
// usernameExpression represents an expression which will be evaluated by CEL.
|
||||
// The expression's result will become the user's username.
|
||||
//
|
||||
// usernameExpression is similar to claimMappings.username.expression from Kubernetes AuthenticationConfiguration
|
||||
// as documented in https://kubernetes.io/docs/reference/access-authn-authz/authentication.
|
||||
// This is an advanced configuration option. During an end-user login flow, each of these CEL expressions
|
||||
// must evaluate to the expected type without errors, or else the user's login will fail.
|
||||
// Additionally, mistakes in this configuration can cause the users to have unintended usernames.
|
||||
//
|
||||
// The expression must produce a non-empty string value.
|
||||
// If the expression uses 'claims.email', then 'claims.email_verified' must be used in
|
||||
// the expression or extra[*].valueExpression or claimValidationRules[*].expression.
|
||||
// An example claim validation rule expression that matches the validation automatically
|
||||
// applied when username.claim is set to 'email' is 'claims.?email_verified.orValue(true) == true'.
|
||||
// By explicitly comparing the value to true, we let type-checking see the result will be a boolean,
|
||||
// and to make sure a non-boolean email_verified claim will be caught at runtime.
|
||||
//
|
||||
// CEL expressions have access to the contents of the token claims, organized into CEL variable:
|
||||
// - 'claims' is a map of claim names to claim values.
|
||||
// For example, a variable named 'sub' can be accessed as 'claims.sub'.
|
||||
// Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// Mutually exclusive with username. Use either username or usernameExpression to
|
||||
// determine the user's username from the JWT token.
|
||||
// +optional
|
||||
UsernameExpression string `json:"usernameExpression,omitempty"`
|
||||
|
||||
// groups is the name of the claim which should be read to extract the user's
|
||||
// group membership from the JWT token. When not specified, it will default to "groups",
|
||||
// unless groupsExpression is specified.
|
||||
//
|
||||
// Mutually exclusive with groupsExpression. Use either groups or groupsExpression to
|
||||
// determine the user's group membership from the JWT token.
|
||||
// +optional
|
||||
Groups string `json:"groups"`
|
||||
|
||||
// groupsExpression represents an expression which will be evaluated by CEL.
|
||||
// The expression's result will become the user's group memberships.
|
||||
//
|
||||
// groupsExpression is similar to claimMappings.groups.expression from Kubernetes AuthenticationConfiguration
|
||||
// as documented in https://kubernetes.io/docs/reference/access-authn-authz/authentication.
|
||||
// This is an advanced configuration option. During an end-user login flow, each of these CEL expressions
|
||||
// must evaluate to one of the expected types without errors, or else the user's login will fail.
|
||||
// Additionally, mistakes in this configuration can cause the users to have unintended group memberships.
|
||||
//
|
||||
// The expression must produce a string or string array value.
|
||||
// "", [], and null values are treated as the group mapping not being present.
|
||||
//
|
||||
// CEL expressions have access to the contents of the token claims, organized into CEL variable:
|
||||
// - 'claims' is a map of claim names to claim values.
|
||||
// For example, a variable named 'sub' can be accessed as 'claims.sub'.
|
||||
// Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// Mutually exclusive with groups. Use either groups or groupsExpression to
|
||||
// determine the user's group membership from the JWT token.
|
||||
// +optional
|
||||
GroupsExpression string `json:"groupsExpression,omitempty"`
|
||||
|
||||
// extra is similar to claimMappings.extra from Kubernetes AuthenticationConfiguration
|
||||
// as documented in https://kubernetes.io/docs/reference/access-authn-authz/authentication.
|
||||
//
|
||||
// However, note that the Pinniped Concierge issues client certificates to users for the purpose
|
||||
// of authenticating, and the Kubernetes API server does not have any mechanism for transmitting
|
||||
// auth extras via client certificates. When configured, these extras will appear in client
|
||||
// certificates issued by the Pinniped Supervisor in the x509 Subject field as Organizational
|
||||
// Units (OU). However, when this client certificate is presented to Kubernetes for authentication,
|
||||
// Kubernetes will ignore these extras. This is probably only useful if you are using a custom
|
||||
// authenticating proxy in front of your Kubernetes API server which can translate these OUs into
|
||||
// auth extras, as described by
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#authenticating-proxy.
|
||||
// This is an advanced configuration option. During an end-user login flow, each of these CEL expressions
|
||||
// must evaluate to either a string or an array of strings, or else the user's login will fail.
|
||||
//
|
||||
// These keys must be a domain-prefixed path (such as "acme.io/foo") and must not contain an equals sign ("=").
|
||||
//
|
||||
// expression must produce a string or string array value.
|
||||
// If the value is empty, the extra mapping will not be present.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// hard-coded extra key/value
|
||||
// - key: "acme.io/foo"
|
||||
// valueExpression: "'bar'"
|
||||
// This will result in an extra attribute - acme.io/foo: ["bar"]
|
||||
//
|
||||
// hard-coded key, value copying claim value
|
||||
// - key: "acme.io/foo"
|
||||
// valueExpression: "claims.some_claim"
|
||||
// This will result in an extra attribute - acme.io/foo: [value of some_claim]
|
||||
//
|
||||
// hard-coded key, value derived from claim value
|
||||
// - key: "acme.io/admin"
|
||||
// valueExpression: '(has(claims.is_admin) && claims.is_admin) ? "true":""'
|
||||
// This will result in:
|
||||
// - if is_admin claim is present and true, extra attribute - acme.io/admin: ["true"]
|
||||
// - if is_admin claim is present and false or is_admin claim is not present, no extra attribute will be added
|
||||
//
|
||||
// +optional
|
||||
Extra []ExtraMapping `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraMapping provides the configuration for a single extra mapping.
|
||||
type ExtraMapping struct {
|
||||
// key is a string to use as the extra attribute key.
|
||||
// key must be a domain-prefix path (e.g. example.org/foo). All characters before the first "/" must be a valid
|
||||
// subdomain as defined by RFC 1123. All characters trailing the first "/" must
|
||||
// be valid HTTP Path characters as defined by RFC 3986.
|
||||
// key must be lowercase.
|
||||
// Required to be unique.
|
||||
// Additionally, the key must not contain an equals sign ("=").
|
||||
// +required
|
||||
Key string `json:"key"`
|
||||
|
||||
// valueExpression is a CEL expression to extract extra attribute value.
|
||||
// valueExpression must produce a string or string array value.
|
||||
// "", [], and null values are treated as the extra mapping not being present.
|
||||
// Empty string values contained within a string array are filtered out.
|
||||
//
|
||||
// CEL expressions have access to the contents of the token claims, organized into CEL variable:
|
||||
// - 'claims' is a map of claim names to claim values.
|
||||
// For example, a variable named 'sub' can be accessed as 'claims.sub'.
|
||||
// Nested claims can be accessed using dot notation, e.g. 'claims.foo.bar'.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// +required
|
||||
ValueExpression string `json:"valueExpression"`
|
||||
}
|
||||
|
||||
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
||||
//
|
||||
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
||||
// signature, existence of claims, etc.) and extract the username and groups from the token.
|
||||
//
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster
|
||||
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
|
||||
// +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type JWTAuthenticator struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// spec for configuring the authenticator.
|
||||
Spec JWTAuthenticatorSpec `json:"spec"`
|
||||
|
||||
// status of the authenticator.
|
||||
Status JWTAuthenticatorStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// JWTAuthenticatorList is a list of JWTAuthenticator objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type JWTAuthenticatorList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []JWTAuthenticator `json:"items"`
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles.
|
||||
//
|
||||
// +kubebuilder:validation:Enum=Secret;ConfigMap
|
||||
type CertificateAuthorityDataSourceKind string
|
||||
|
||||
const (
|
||||
// CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles.
|
||||
CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap")
|
||||
|
||||
// CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles.
|
||||
// Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque.
|
||||
CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret")
|
||||
)
|
||||
|
||||
// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification.
|
||||
type CertificateAuthorityDataSourceSpec struct {
|
||||
// Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
|
||||
// Allowed values are "Secret" or "ConfigMap".
|
||||
// "ConfigMap" uses a Kubernetes configmap to source CA Bundles.
|
||||
// "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
|
||||
Kind CertificateAuthorityDataSourceKind `json:"kind"`
|
||||
// Name is the resource name of the secret or configmap from which to read the CA bundle.
|
||||
// The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Name string `json:"name"`
|
||||
// Key is the key name within the secret or configmap from which to read the CA bundle.
|
||||
// The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
|
||||
// certificate bundle.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// TLSSpec provides TLS configuration on various authenticators.
|
||||
type TLSSpec struct {
|
||||
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||
// +optional
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
|
||||
// Reference to a CA bundle in a secret or a configmap.
|
||||
// Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
|
||||
// +optional
|
||||
CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"`
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type WebhookAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
|
||||
WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
|
||||
|
||||
// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
|
||||
WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
|
||||
|
||||
// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
|
||||
WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a webhook authenticator.
|
||||
type WebhookAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the WebhookAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a webhook authenticator.
|
||||
type WebhookAuthenticatorSpec struct {
|
||||
// Webhook server endpoint URL.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://`
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// TLS configuration.
|
||||
// +optional
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookAuthenticator describes the configuration of a webhook authenticator.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster
|
||||
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type WebhookAuthenticator struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec for configuring the authenticator.
|
||||
Spec WebhookAuthenticatorSpec `json:"spec"`
|
||||
|
||||
// Status of the authenticator.
|
||||
Status WebhookAuthenticatorStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of WebhookAuthenticator objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WebhookAuthenticatorList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []WebhookAuthenticator `json:"items"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=config.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration API.
|
||||
package v1alpha1
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "config.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&CredentialIssuer{},
|
||||
&CredentialIssuerList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// StrategyType enumerates a type of "strategy" used to implement credential access on a cluster.
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||
type StrategyType string
|
||||
|
||||
// FrontendType enumerates a type of "frontend" used to provide access to users of a cluster.
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||
type FrontendType string
|
||||
|
||||
// StrategyStatus enumerates whether a strategy is working on a cluster.
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// StrategyReason enumerates the detailed reason why a strategy is in a particular status.
|
||||
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||
|
||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
ListeningStrategyReason = StrategyReason("Listening")
|
||||
PendingStrategyReason = StrategyReason("Pending")
|
||||
DisabledStrategyReason = StrategyReason("Disabled")
|
||||
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
)
|
||||
|
||||
// CredentialIssuerSpec describes the intended configuration of the Concierge.
|
||||
type CredentialIssuerSpec struct {
|
||||
// ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy.
|
||||
ImpersonationProxy *ImpersonationProxySpec `json:"impersonationProxy"`
|
||||
}
|
||||
|
||||
// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy.
|
||||
// Allowed values are "auto", "enabled", or "disabled".
|
||||
//
|
||||
// +kubebuilder:validation:Enum=auto;enabled;disabled
|
||||
type ImpersonationProxyMode string
|
||||
|
||||
const (
|
||||
// ImpersonationProxyModeDisabled explicitly disables the impersonation proxy.
|
||||
ImpersonationProxyModeDisabled = ImpersonationProxyMode("disabled")
|
||||
|
||||
// ImpersonationProxyModeEnabled explicitly enables the impersonation proxy.
|
||||
ImpersonationProxyModeEnabled = ImpersonationProxyMode("enabled")
|
||||
|
||||
// ImpersonationProxyModeAuto enables or disables the impersonation proxy based upon the cluster in which it is running.
|
||||
ImpersonationProxyModeAuto = ImpersonationProxyMode("auto")
|
||||
)
|
||||
|
||||
// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy.
|
||||
// Allowed values are "LoadBalancer", "ClusterIP", or "None".
|
||||
//
|
||||
// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None
|
||||
type ImpersonationProxyServiceType string
|
||||
|
||||
const (
|
||||
// ImpersonationProxyServiceTypeLoadBalancer provisions a service of type LoadBalancer.
|
||||
ImpersonationProxyServiceTypeLoadBalancer = ImpersonationProxyServiceType("LoadBalancer")
|
||||
|
||||
// ImpersonationProxyServiceTypeClusterIP provisions a service of type ClusterIP.
|
||||
ImpersonationProxyServiceTypeClusterIP = ImpersonationProxyServiceType("ClusterIP")
|
||||
|
||||
// ImpersonationProxyServiceTypeNone does not automatically provision any service.
|
||||
ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None")
|
||||
)
|
||||
|
||||
// ImpersonationProxyTLSSpec contains information about how the Concierge impersonation proxy should
|
||||
// serve TLS.
|
||||
//
|
||||
// If CertificateAuthorityData is not provided, the Concierge impersonation proxy will check the secret
|
||||
// for a field called "ca.crt", which will be used as the CertificateAuthorityData.
|
||||
//
|
||||
// If neither CertificateAuthorityData nor ca.crt is provided, no CA bundle will be advertised for
|
||||
// the impersonation proxy endpoint.
|
||||
type ImpersonationProxyTLSSpec struct {
|
||||
// X.509 Certificate Authority (base64-encoded PEM bundle).
|
||||
// Used to advertise the CA bundle for the impersonation proxy endpoint.
|
||||
//
|
||||
// +optional
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
|
||||
|
||||
// SecretName is the name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains
|
||||
// the TLS serving certificate for the Concierge impersonation proxy endpoint.
|
||||
//
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
}
|
||||
|
||||
// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy.
|
||||
type ImpersonationProxySpec struct {
|
||||
// Mode configures whether the impersonation proxy should be started:
|
||||
// - "disabled" explicitly disables the impersonation proxy. This is the default.
|
||||
// - "enabled" explicitly enables the impersonation proxy.
|
||||
// - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running.
|
||||
Mode ImpersonationProxyMode `json:"mode"`
|
||||
|
||||
// Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients.
|
||||
//
|
||||
// +kubebuilder:default:={"type": "LoadBalancer"}
|
||||
Service ImpersonationProxyServiceSpec `json:"service"`
|
||||
|
||||
// ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will
|
||||
// be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
//
|
||||
// This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
//
|
||||
// +optional
|
||||
ExternalEndpoint string `json:"externalEndpoint,omitempty"`
|
||||
|
||||
// TLS contains information about how the Concierge impersonation proxy should serve TLS.
|
||||
//
|
||||
// If this field is empty, the impersonation proxy will generate its own TLS certificate.
|
||||
//
|
||||
// +optional
|
||||
TLS *ImpersonationProxyTLSSpec `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy.
|
||||
type ImpersonationProxyServiceSpec struct {
|
||||
// Type specifies the type of Service to provision for the impersonation proxy.
|
||||
//
|
||||
// If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty
|
||||
// value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status.
|
||||
//
|
||||
// +kubebuilder:default:="LoadBalancer"
|
||||
Type ImpersonationProxyServiceType `json:"type,omitempty"`
|
||||
|
||||
// LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service.
|
||||
// This is not supported on all cloud providers.
|
||||
//
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:MaxLength=255
|
||||
// +optional
|
||||
LoadBalancerIP string `json:"loadBalancerIP,omitempty"`
|
||||
|
||||
// Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service.
|
||||
//
|
||||
// +optional
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// CredentialIssuerStatus describes the status of the Concierge.
|
||||
type CredentialIssuerStatus struct {
|
||||
// List of integration strategies that were attempted by Pinniped.
|
||||
Strategies []CredentialIssuerStrategy `json:"strategies"`
|
||||
}
|
||||
|
||||
// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped.
|
||||
type CredentialIssuerStrategy struct {
|
||||
// Type of integration attempted.
|
||||
Type StrategyType `json:"type"`
|
||||
|
||||
// Status of the attempted integration strategy.
|
||||
Status StrategyStatus `json:"status"`
|
||||
|
||||
// Reason for the current status.
|
||||
Reason StrategyReason `json:"reason"`
|
||||
|
||||
// Human-readable description of the current status.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Message string `json:"message"`
|
||||
|
||||
// When the status was last checked.
|
||||
LastUpdateTime metav1.Time `json:"lastUpdateTime"`
|
||||
|
||||
// Frontend describes how clients can connect using this strategy.
|
||||
Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"`
|
||||
}
|
||||
|
||||
// CredentialIssuerFrontend describes how to connect using a particular integration strategy.
|
||||
type CredentialIssuerFrontend struct {
|
||||
// Type describes which frontend mechanism clients can use with a strategy.
|
||||
Type FrontendType `json:"type"`
|
||||
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
||||
|
||||
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||
// This field is only set when Type is "ImpersonationProxy".
|
||||
ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"`
|
||||
}
|
||||
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
type TokenCredentialRequestAPIInfo struct {
|
||||
// Server is the Kubernetes API server URL.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
Server string `json:"server"`
|
||||
|
||||
// CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
||||
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||
type ImpersonationProxyInfo struct {
|
||||
// Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://`
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
||||
// CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped,scope=Cluster
|
||||
// +kubebuilder:printcolumn:name="ProxyMode",type=string,JSONPath=`.spec.impersonationProxy.mode`
|
||||
// +kubebuilder:printcolumn:name="DefaultStrategy",type=string,JSONPath=`.status.strategies[?(@.status == "Success")].type`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type CredentialIssuer struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec describes the intended configuration of the Concierge.
|
||||
//
|
||||
// +optional
|
||||
Spec CredentialIssuerSpec `json:"spec"`
|
||||
|
||||
// CredentialIssuerStatus describes the status of the Concierge.
|
||||
//
|
||||
// +optional
|
||||
Status CredentialIssuerStatus `json:"status"`
|
||||
}
|
||||
|
||||
// CredentialIssuerList is a list of CredentialIssuer objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type CredentialIssuerList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []CredentialIssuer `json:"items"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=identity.concierge.pinniped.dev
|
||||
|
||||
// Package identity is the internal version of the Pinniped identity API.
|
||||
package identity
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "identity.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||
|
||||
// Kind takes an unqualified kind and returns back a Group qualified GroupKind.
|
||||
func Kind(kind string) schema.GroupKind {
|
||||
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns back a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WhoAmIRequest{},
|
||||
&WhoAmIRequestList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import "fmt"
|
||||
|
||||
// KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it.
|
||||
// Copied from the Kubernetes token review API.
|
||||
type KubernetesUserInfo struct {
|
||||
// User is the UserInfo associated with the current user.
|
||||
User UserInfo
|
||||
// Audiences are audience identifiers chosen by the authenticator.
|
||||
Audiences []string
|
||||
}
|
||||
|
||||
// UserInfo holds the information about the user needed to implement the
|
||||
// user.Info interface.
|
||||
type UserInfo struct {
|
||||
// The name that uniquely identifies this user among all active users.
|
||||
Username string
|
||||
// A unique value that identifies this user across time. If this user is
|
||||
// deleted and another user by the same name is added, they will have
|
||||
// different UIDs.
|
||||
UID string
|
||||
// The names of groups this user is a part of.
|
||||
Groups []string
|
||||
// Any additional information provided by the authenticator.
|
||||
Extra map[string]ExtraValue
|
||||
}
|
||||
|
||||
// ExtraValue masks the value so protobuf can generate
|
||||
type ExtraValue []string
|
||||
|
||||
func (t ExtraValue) String() string {
|
||||
return fmt.Sprintf("%v", []string(t))
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// WhoAmIRequest submits a request to echo back the current authenticated user.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequest struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ObjectMeta
|
||||
|
||||
Spec WhoAmIRequestSpec
|
||||
Status WhoAmIRequestStatus
|
||||
}
|
||||
|
||||
// Spec is always empty for a WhoAmIRequest.
|
||||
type WhoAmIRequestSpec struct {
|
||||
// empty for now but we may add some config here in the future
|
||||
// any such config must be safe in the context of an unauthenticated user
|
||||
}
|
||||
|
||||
// Status is set by the server in the response to a WhoAmIRequest.
|
||||
type WhoAmIRequestStatus struct {
|
||||
// The current authenticated user, exactly as Kubernetes understands it.
|
||||
KubernetesUserInfo KubernetesUserInfo
|
||||
|
||||
// We may add concierge specific information here in the future.
|
||||
}
|
||||
|
||||
// WhoAmIRequestList is a list of WhoAmIRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequestList struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ListMeta
|
||||
|
||||
// Items is a list of WhoAmIRequest.
|
||||
Items []WhoAmIRequest
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/concierge/identity
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +k8s:openapi-model-package=dev.pinniped.apis.concierge.identity.v1alpha1
|
||||
// +groupName=identity.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped identity API.
|
||||
package v1alpha1
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "identity.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WhoAmIRequest{},
|
||||
&WhoAmIRequestList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import "fmt"
|
||||
|
||||
// KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it.
|
||||
// Copied from the Kubernetes token review API.
|
||||
type KubernetesUserInfo struct {
|
||||
// User is the UserInfo associated with the current user.
|
||||
User UserInfo `json:"user"`
|
||||
// Audiences are audience identifiers chosen by the authenticator.
|
||||
// +optional
|
||||
Audiences []string `json:"audiences,omitempty"`
|
||||
}
|
||||
|
||||
// UserInfo holds the information about the user needed to implement the
|
||||
// user.Info interface.
|
||||
type UserInfo struct {
|
||||
// The name that uniquely identifies this user among all active users.
|
||||
Username string `json:"username"`
|
||||
// A unique value that identifies this user across time. If this user is
|
||||
// deleted and another user by the same name is added, they will have
|
||||
// different UIDs.
|
||||
// +optional
|
||||
UID string `json:"uid,omitempty"`
|
||||
// The names of groups this user is a part of.
|
||||
// +optional
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// Any additional information provided by the authenticator.
|
||||
// +optional
|
||||
Extra map[string]ExtraValue `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraValue masks the value so protobuf can generate
|
||||
type ExtraValue []string
|
||||
|
||||
func (t ExtraValue) String() string {
|
||||
return fmt.Sprintf("%v", []string(t))
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// WhoAmIRequest submits a request to echo back the current authenticated user.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +genclient:onlyVerbs=create
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequest struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec WhoAmIRequestSpec `json:"spec,omitempty"`
|
||||
Status WhoAmIRequestStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// Spec is always empty for a WhoAmIRequest.
|
||||
type WhoAmIRequestSpec struct {
|
||||
// empty for now but we may add some config here in the future
|
||||
// any such config must be safe in the context of an unauthenticated user
|
||||
}
|
||||
|
||||
// Status is set by the server in the response to a WhoAmIRequest.
|
||||
type WhoAmIRequestStatus struct {
|
||||
// The current authenticated user, exactly as Kubernetes understands it.
|
||||
KubernetesUserInfo KubernetesUserInfo `json:"kubernetesUserInfo"`
|
||||
|
||||
// We may add concierge specific information here in the future.
|
||||
}
|
||||
|
||||
// WhoAmIRequestList is a list of WhoAmIRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequestList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Items is a list of WhoAmIRequest.
|
||||
Items []WhoAmIRequest `json:"items"`
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
identityapi "go.pinniped.dev/GENERATED_PKG/apis/concierge/identity"
|
||||
)
|
||||
|
||||
func ValidateWhoAmIRequest(whoAmIRequest *identityapi.WhoAmIRequest) field.ErrorList {
|
||||
return nil // add validation for spec here if we expand it
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=login.concierge.pinniped.dev
|
||||
|
||||
// Package login is the internal version of the Pinniped login API.
|
||||
package login
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package login
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "login.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||
|
||||
// Kind takes an unqualified kind and returns back a Group qualified GroupKind.
|
||||
func Kind(kind string) schema.GroupKind {
|
||||
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns back a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&TokenCredentialRequest{},
|
||||
&TokenCredentialRequestList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package login
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// ClusterCredential is the cluster-specific credential returned on a successful credential request. It
|
||||
// contains either a valid bearer token or a valid TLS certificate and corresponding private key for the cluster.
|
||||
type ClusterCredential struct {
|
||||
// ExpirationTimestamp indicates a time when the provided credentials expire.
|
||||
ExpirationTimestamp metav1.Time
|
||||
|
||||
// Token is a bearer token used by the client for request authentication.
|
||||
Token string
|
||||
|
||||
// PEM-encoded client TLS certificates (including intermediates, if any).
|
||||
ClientCertificateData string
|
||||
|
||||
// PEM-encoded private key for the above certificate.
|
||||
ClientKeyData string
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package login
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Specification of a TokenCredentialRequest, expected on requests to the Pinniped API.
|
||||
type TokenCredentialRequestSpec struct {
|
||||
// Bearer token supplied with the credential request.
|
||||
Token string
|
||||
|
||||
// Reference to an authenticator which can validate this credential request.
|
||||
Authenticator corev1.TypedLocalObjectReference
|
||||
}
|
||||
|
||||
// Status of a TokenCredentialRequest, returned on responses to the Pinniped API.
|
||||
type TokenCredentialRequestStatus struct {
|
||||
// A Credential will be returned for a successful credential request.
|
||||
// +optional
|
||||
Credential *ClusterCredential
|
||||
|
||||
// An error message will be returned for an unsuccessful credential request.
|
||||
// +optional
|
||||
Message *string
|
||||
}
|
||||
|
||||
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequest struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ObjectMeta
|
||||
|
||||
Spec TokenCredentialRequestSpec
|
||||
Status TokenCredentialRequestStatus
|
||||
}
|
||||
|
||||
// TokenCredentialRequestList is a list of TokenCredentialRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequestList struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ListMeta
|
||||
|
||||
// Items is a list of TokenCredentialRequest.
|
||||
Items []TokenCredentialRequest
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/concierge/login
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +k8s:openapi-model-package=dev.pinniped.apis.concierge.login.v1alpha1
|
||||
// +groupName=login.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped login API.
|
||||
package v1alpha1
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "login.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&TokenCredentialRequest{},
|
||||
&TokenCredentialRequestList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// ClusterCredential is the cluster-specific credential returned on a successful credential request. It
|
||||
// contains either a valid bearer token or a valid TLS certificate and corresponding private key for the cluster.
|
||||
type ClusterCredential struct {
|
||||
// ExpirationTimestamp indicates a time when the provided credentials expire.
|
||||
ExpirationTimestamp metav1.Time `json:"expirationTimestamp,omitempty"`
|
||||
|
||||
// Token is a bearer token used by the client for request authentication.
|
||||
Token string `json:"token,omitempty"`
|
||||
|
||||
// PEM-encoded client TLS certificates (including intermediates, if any).
|
||||
ClientCertificateData string `json:"clientCertificateData,omitempty"`
|
||||
|
||||
// PEM-encoded private key for the above certificate.
|
||||
ClientKeyData string `json:"clientKeyData,omitempty"`
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Specification of a TokenCredentialRequest, expected on requests to the Pinniped API.
|
||||
type TokenCredentialRequestSpec struct {
|
||||
// Bearer token supplied with the credential request.
|
||||
Token string `json:"token,omitempty"`
|
||||
|
||||
// Reference to an authenticator which can validate this credential request.
|
||||
Authenticator corev1.TypedLocalObjectReference `json:"authenticator"`
|
||||
}
|
||||
|
||||
// Status of a TokenCredentialRequest, returned on responses to the Pinniped API.
|
||||
type TokenCredentialRequestStatus struct {
|
||||
// A Credential will be returned for a successful credential request.
|
||||
// +optional
|
||||
Credential *ClusterCredential `json:"credential,omitempty"`
|
||||
|
||||
// An error message will be returned for an unsuccessful credential request.
|
||||
// +optional
|
||||
Message *string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +genclient:onlyVerbs=create
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequest struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec TokenCredentialRequestSpec `json:"spec,omitempty"`
|
||||
Status TokenCredentialRequestStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// TokenCredentialRequestList is a list of TokenCredentialRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequestList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Items is a list of TokenCredentialRequest.
|
||||
Items []TokenCredentialRequest `json:"items"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=clientsecret.supervisor.pinniped.dev
|
||||
|
||||
// Package clientsecret is the internal version of the Pinniped client secret API.
|
||||
package clientsecret
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package clientsecret
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "clientsecret.supervisor.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||
|
||||
// Kind takes an unqualified kind and returns back a Group qualified GroupKind.
|
||||
func Kind(kind string) schema.GroupKind {
|
||||
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns back a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&OIDCClientSecretRequest{},
|
||||
&OIDCClientSecretRequestList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package clientsecret
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// OIDCClientSecretRequest can be used to update the client secrets associated with an OIDCClient.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCClientSecretRequest struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ObjectMeta // metadata.name must be set to the client ID
|
||||
|
||||
Spec OIDCClientSecretRequestSpec
|
||||
|
||||
// +optional
|
||||
Status OIDCClientSecretRequestStatus
|
||||
}
|
||||
|
||||
// Spec of the OIDCClientSecretRequest.
|
||||
type OIDCClientSecretRequestSpec struct {
|
||||
// Request a new client secret to for the OIDCClient referenced by the metadata.name field.
|
||||
// +optional
|
||||
GenerateNewSecret bool
|
||||
|
||||
// Revoke the old client secrets associated with the OIDCClient referenced by the metadata.name field.
|
||||
// +optional
|
||||
RevokeOldSecrets bool
|
||||
}
|
||||
|
||||
// Status of the OIDCClientSecretRequest.
|
||||
type OIDCClientSecretRequestStatus struct {
|
||||
// The unencrypted OIDC Client Secret. This will only be shared upon creation and cannot be recovered if lost.
|
||||
GeneratedSecret string
|
||||
|
||||
// The total number of client secrets associated with the OIDCClient referenced by the metadata.name field.
|
||||
TotalClientSecrets int
|
||||
}
|
||||
|
||||
// OIDCClientSecretRequestList is a list of OIDCClientSecretRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCClientSecretRequestList struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ListMeta
|
||||
|
||||
// Items is a list of OIDCClientSecretRequest.
|
||||
Items []OIDCClientSecretRequest
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/supervisor/clientsecret
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +k8s:openapi-model-package=dev.pinniped.apis.supervisor.clientsecret.v1alpha1
|
||||
// +groupName=clientsecret.supervisor.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped client secret API.
|
||||
package v1alpha1
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "clientsecret.supervisor.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&OIDCClientSecretRequest{},
|
||||
&OIDCClientSecretRequestList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns back a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// OIDCClientSecretRequest can be used to update the client secrets associated with an OIDCClient.
|
||||
// +genclient
|
||||
// +genclient:onlyVerbs=create
|
||||
// +kubebuilder:subresource:status
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCClientSecretRequest struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"` // metadata.name must be set to the client ID
|
||||
|
||||
Spec OIDCClientSecretRequestSpec `json:"spec"`
|
||||
|
||||
// +optional
|
||||
Status OIDCClientSecretRequestStatus `json:"status"`
|
||||
}
|
||||
|
||||
// Spec of the OIDCClientSecretRequest.
|
||||
type OIDCClientSecretRequestSpec struct {
|
||||
// Request a new client secret to for the OIDCClient referenced by the metadata.name field.
|
||||
// +optional
|
||||
GenerateNewSecret bool `json:"generateNewSecret"`
|
||||
|
||||
// Revoke the old client secrets associated with the OIDCClient referenced by the metadata.name field.
|
||||
// +optional
|
||||
RevokeOldSecrets bool `json:"revokeOldSecrets"`
|
||||
}
|
||||
|
||||
// Status of the OIDCClientSecretRequest.
|
||||
type OIDCClientSecretRequestStatus struct {
|
||||
// The unencrypted OIDC Client Secret. This will only be shared upon creation and cannot be recovered if lost.
|
||||
GeneratedSecret string `json:"generatedSecret,omitempty"`
|
||||
|
||||
// The total number of client secrets associated with the OIDCClient referenced by the metadata.name field.
|
||||
TotalClientSecrets int `json:"totalClientSecrets"`
|
||||
}
|
||||
|
||||
// OIDCClientSecretRequestList is a list of OIDCClientSecretRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCClientSecretRequestList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Items is a list of OIDCClientSecretRequest.
|
||||
Items []OIDCClientSecretRequest `json:"items"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=config.supervisor.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuration API.
|
||||
package v1alpha1
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "config.supervisor.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&FederationDomain{},
|
||||
&FederationDomainList{},
|
||||
&OIDCClient{},
|
||||
&OIDCClientList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type FederationDomainPhase string
|
||||
|
||||
const (
|
||||
// FederationDomainPhasePending is the default phase for newly-created FederationDomain resources.
|
||||
FederationDomainPhasePending FederationDomainPhase = "Pending"
|
||||
|
||||
// FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state.
|
||||
FederationDomainPhaseReady FederationDomainPhase = "Ready"
|
||||
|
||||
// FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state.
|
||||
FederationDomainPhaseError FederationDomainPhase = "Error"
|
||||
)
|
||||
|
||||
// FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider.
|
||||
type FederationDomainTLSSpec struct {
|
||||
// SecretName is an optional name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains
|
||||
// the TLS serving certificate for the HTTPS endpoints served by this FederationDomain. When provided, the TLS Secret
|
||||
// named here must contain keys named `tls.crt` and `tls.key` that contain the certificate and private key to use
|
||||
// for TLS.
|
||||
//
|
||||
// Server Name Indication (SNI) is an extension to the Transport Layer Security (TLS) supported by all major browsers.
|
||||
//
|
||||
// SecretName is required if you would like to use different TLS certificates for issuers of different hostnames.
|
||||
// SNI requests do not include port numbers, so all issuers with the same DNS hostname must use the same
|
||||
// SecretName value even if they have different port numbers.
|
||||
//
|
||||
// SecretName is not required when you would like to use only the HTTP endpoints (e.g. when the HTTP listener is
|
||||
// configured to listen on loopback interfaces or UNIX domain sockets for traffic from a service mesh sidecar).
|
||||
// It is also not required when you would like all requests to this OIDC Provider's HTTPS endpoints to
|
||||
// use the default TLS certificate, which is configured elsewhere.
|
||||
//
|
||||
// When your Issuer URL's host is an IP address, then this field is ignored. SNI does not work for IP addresses.
|
||||
//
|
||||
// +optional
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to
|
||||
// the transform expressions. This is a union type, and Type is the discriminator field.
|
||||
type FederationDomainTransformsConstant struct {
|
||||
// Name determines the name of the constant. It must be a valid identifier name.
|
||||
// +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$`
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:MaxLength=64
|
||||
Name string `json:"name"`
|
||||
|
||||
// Type determines the type of the constant, and indicates which other field should be non-empty.
|
||||
// Allowed values are "string" or "stringList".
|
||||
// +kubebuilder:validation:Enum=string;stringList
|
||||
Type string `json:"type"`
|
||||
|
||||
// StringValue should hold the value when Type is "string", and is otherwise ignored.
|
||||
// +optional
|
||||
StringValue string `json:"stringValue,omitempty"`
|
||||
|
||||
// StringListValue should hold the value when Type is "stringList", and is otherwise ignored.
|
||||
// +optional
|
||||
StringListValue []string `json:"stringListValue,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainTransformsExpression defines a transform expression.
|
||||
type FederationDomainTransformsExpression struct {
|
||||
// Type determines the type of the expression. It must be one of the supported types.
|
||||
// Allowed values are "policy/v1", "username/v1", or "groups/v1".
|
||||
// +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1
|
||||
Type string `json:"type"`
|
||||
|
||||
// Expression is a CEL expression that will be evaluated based on the Type during an authentication.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Expression string `json:"expression"`
|
||||
|
||||
// Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects
|
||||
// an authentication attempt. When empty, a default message will be used.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainTransformsExample defines a transform example.
|
||||
type FederationDomainTransformsExample struct {
|
||||
// Username is the input username.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Username string `json:"username"`
|
||||
|
||||
// Groups is the input list of group names.
|
||||
// +optional
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
|
||||
// Expects is the expected output of the entire sequence of transforms when they are run against the
|
||||
// input Username and Groups.
|
||||
Expects FederationDomainTransformsExampleExpects `json:"expects"`
|
||||
}
|
||||
|
||||
// FederationDomainTransformsExampleExpects defines the expected result for a transforms example.
|
||||
type FederationDomainTransformsExampleExpects struct {
|
||||
// Username is the expected username after the transformations have been applied.
|
||||
// +optional
|
||||
Username string `json:"username,omitempty"`
|
||||
|
||||
// Groups is the expected list of group names after the transformations have been applied.
|
||||
// +optional
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
|
||||
// Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression
|
||||
// after the transformations have been applied. True means that it is expected that the authentication would be
|
||||
// rejected. The default value of false means that it is expected that the authentication would not be rejected
|
||||
// by any policy expression.
|
||||
// +optional
|
||||
Rejected bool `json:"rejected,omitempty"`
|
||||
|
||||
// Message is the expected error message of the transforms. When Rejected is true, then Message is the expected
|
||||
// message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank,
|
||||
// then Message will be treated as the default error message for authentication attempts which are rejected by a
|
||||
// policy. When Rejected is false, then Message is the expected error message for some other non-policy
|
||||
// transformation error, such as a runtime error. When Rejected is false, there is no default expected Message.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain.
|
||||
type FederationDomainTransforms struct {
|
||||
// Constants defines constant variables and their values which will be made available to the transform expressions.
|
||||
// +patchMergeKey=name
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=name
|
||||
// +optional
|
||||
Constants []FederationDomainTransformsConstant `json:"constants,omitempty"`
|
||||
|
||||
// Expressions are an optional list of transforms and policies to be executed in the order given during every
|
||||
// authentication attempt, including during every session refresh.
|
||||
// Each is a CEL expression. It may use the basic CEL language as defined in
|
||||
// https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in
|
||||
// https://github.com/google/cel-go/tree/master/ext#strings.
|
||||
//
|
||||
// The username and groups extracted from the identity provider, and the constants defined in this CR, are
|
||||
// available as variables in all expressions. The username is provided via a variable called `username` and
|
||||
// the list of group names is provided via a variable called `groups` (which may be an empty list).
|
||||
// Each user-provided constants is provided via a variable named `strConst.varName` for string constants
|
||||
// and `strListConst.varName` for string list constants.
|
||||
//
|
||||
// The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1.
|
||||
// Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated
|
||||
// and the authentication attempt is rejected.
|
||||
// Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the
|
||||
// username or group names.
|
||||
// Each username/v1 transform must return the new username (a string), which can be the same as the old username.
|
||||
// Transformations of type username/v1 do not return group names, and therefore cannot change the group names.
|
||||
// Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old
|
||||
// groups list.
|
||||
// Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames.
|
||||
// After each expression, the new (potentially changed) username or groups get passed to the following expression.
|
||||
//
|
||||
// Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain.
|
||||
// During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the
|
||||
// authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username
|
||||
// and group names have been decided for that authentication attempt.
|
||||
//
|
||||
// +optional
|
||||
Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"`
|
||||
|
||||
// Examples can optionally be used to ensure that the sequence of transformation expressions are working as
|
||||
// expected. Examples define sample input identities which are then run through the expression list, and the
|
||||
// results are compared to the expected results. If any example in this list fails, then this
|
||||
// identity provider will not be available for use within this FederationDomain, and the error(s) will be
|
||||
// added to the FederationDomain status. This can be used to help guard against programming mistakes in the
|
||||
// expressions, and also act as living documentation for other administrators to better understand the expressions.
|
||||
// +optional
|
||||
Examples []FederationDomainTransformsExample `json:"examples,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain.
|
||||
type FederationDomainIdentityProvider struct {
|
||||
// DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the
|
||||
// kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a
|
||||
// disruptive change for those users.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
DisplayName string `json:"displayName"`
|
||||
|
||||
// ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required.
|
||||
// If the reference cannot be resolved then the identity provider will not be made available.
|
||||
// Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider,
|
||||
// LDAPIdentityProvider, ActiveDirectoryIdentityProvider.
|
||||
ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"`
|
||||
|
||||
// Transforms is an optional way to specify transformations to be applied during user authentication and
|
||||
// session refresh.
|
||||
// +optional
|
||||
Transforms FederationDomainTransforms `json:"transforms,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainSpec is a struct that describes an OIDC Provider.
|
||||
type FederationDomainSpec struct {
|
||||
// Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the
|
||||
// identifier that it will use for the iss claim in issued JWTs. This field will also be used as
|
||||
// the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is
|
||||
// https://example.com/foo, then your authorization endpoint will look like
|
||||
// https://example.com/foo/some/path/to/auth/endpoint).
|
||||
//
|
||||
// See
|
||||
// https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:XValidation:message="issuer must be an HTTPS URL",rule="isURL(self) && url(self).getScheme() == 'https'"
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain.
|
||||
// +optional
|
||||
TLS *FederationDomainTLSSpec `json:"tls,omitempty"`
|
||||
|
||||
// IdentityProviders is the list of identity providers available for use by this FederationDomain.
|
||||
//
|
||||
// An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server,
|
||||
// how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to
|
||||
// extract a normalized user identity. Normalized user identities include a username and a list of group names.
|
||||
// In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which
|
||||
// belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations
|
||||
// on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid
|
||||
// accidental conflicts when multiple identity providers have different users with the same username (e.g.
|
||||
// "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication
|
||||
// rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow
|
||||
// the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could
|
||||
// disallow the authentication unless the user belongs to a specific group in the identity provider.
|
||||
//
|
||||
// For backwards compatibility with versions of Pinniped which predate support for multiple identity providers,
|
||||
// an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which
|
||||
// exist in the same namespace, but also to reject all authentication requests when there is more than one identity
|
||||
// provider currently defined. In this backwards compatibility mode, the name of the identity provider resource
|
||||
// (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this
|
||||
// FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of
|
||||
// relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead
|
||||
// explicitly list the identity provider using this IdentityProviders field.
|
||||
//
|
||||
// +optional
|
||||
IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainSecrets holds information about this OIDC Provider's secrets.
|
||||
type FederationDomainSecrets struct {
|
||||
// JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are
|
||||
// stored. If it is empty, then the signing/verification keys are either unknown or they don't
|
||||
// exist.
|
||||
// +optional
|
||||
JWKS corev1.LocalObjectReference `json:"jwks,omitempty"`
|
||||
|
||||
// TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
|
||||
// signing tokens is stored.
|
||||
// +optional
|
||||
TokenSigningKey corev1.LocalObjectReference `json:"tokenSigningKey,omitempty"`
|
||||
|
||||
// StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
|
||||
// signing state parameters is stored.
|
||||
// +optional
|
||||
StateSigningKey corev1.LocalObjectReference `json:"stateSigningKey,omitempty"`
|
||||
|
||||
// StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
|
||||
// encrypting state parameters is stored.
|
||||
// +optional
|
||||
StateEncryptionKey corev1.LocalObjectReference `json:"stateEncryptionKey,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainStatus is a struct that describes the actual state of an OIDC Provider.
|
||||
type FederationDomainStatus struct {
|
||||
// Phase summarizes the overall status of the FederationDomain.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase FederationDomainPhase `json:"phase,omitempty"`
|
||||
|
||||
// Conditions represent the observations of an FederationDomain's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
|
||||
// Secrets contains information about this OIDC Provider's secrets.
|
||||
// +optional
|
||||
Secrets FederationDomainSecrets `json:"secrets,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomain describes the configuration of an OIDC provider.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped
|
||||
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type FederationDomain struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec of the OIDC provider.
|
||||
Spec FederationDomainSpec `json:"spec"`
|
||||
|
||||
// Status of the OIDC provider.
|
||||
Status FederationDomainStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of FederationDomain objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type FederationDomainList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []FederationDomain `json:"items"`
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type OIDCClientPhase string
|
||||
|
||||
const (
|
||||
// OIDCClientPhasePending is the default phase for newly-created OIDCClient resources.
|
||||
OIDCClientPhasePending OIDCClientPhase = "Pending"
|
||||
|
||||
// OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state.
|
||||
OIDCClientPhaseReady OIDCClientPhase = "Ready"
|
||||
|
||||
// OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state.
|
||||
OIDCClientPhaseError OIDCClientPhase = "Error"
|
||||
)
|
||||
|
||||
// +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/`
|
||||
type RedirectURI string
|
||||
|
||||
// +kubebuilder:validation:Enum="authorization_code";"refresh_token";"urn:ietf:params:oauth:grant-type:token-exchange"
|
||||
type GrantType string
|
||||
|
||||
// +kubebuilder:validation:Enum="openid";"offline_access";"username";"groups";"pinniped:request-audience"
|
||||
type Scope string
|
||||
|
||||
// OIDCClientSpec is a struct that describes an OIDCClient.
|
||||
type OIDCClientSpec struct {
|
||||
// allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this
|
||||
// client. Any other uris will be rejected.
|
||||
// Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme.
|
||||
// Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri.
|
||||
// +listType=set
|
||||
// +kubebuilder:validation:MinItems=1
|
||||
AllowedRedirectURIs []RedirectURI `json:"allowedRedirectURIs"`
|
||||
|
||||
// allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this
|
||||
// client.
|
||||
//
|
||||
// Must only contain the following values:
|
||||
// - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to
|
||||
// authenticate users. This grant must always be listed.
|
||||
// - refresh_token: allows the client to perform refresh grants for the user to extend the user's session.
|
||||
// This grant must be listed if allowedScopes lists offline_access.
|
||||
// - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange,
|
||||
// which is a step in the process to be able to get a cluster credential for the user.
|
||||
// This grant must be listed if allowedScopes lists pinniped:request-audience.
|
||||
// +listType=set
|
||||
// +kubebuilder:validation:MinItems=1
|
||||
AllowedGrantTypes []GrantType `json:"allowedGrantTypes"`
|
||||
|
||||
// allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client.
|
||||
//
|
||||
// Must only contain the following values:
|
||||
// - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat).
|
||||
// This scope must always be listed.
|
||||
// - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow.
|
||||
// This scope must be listed if allowedGrantTypes lists refresh_token.
|
||||
// - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange,
|
||||
// which is a step in the process to be able to get a cluster credential for the user.
|
||||
// openid, username and groups scopes must be listed when this scope is present.
|
||||
// This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange.
|
||||
// - username: The client is allowed to request that ID tokens contain the user's username.
|
||||
// Without the username scope being requested and allowed, the ID token will not contain the user's username.
|
||||
// - groups: The client is allowed to request that ID tokens contain the user's group membership,
|
||||
// if their group membership is discoverable by the Supervisor.
|
||||
// Without the groups scope being requested and allowed, the ID token will not contain groups.
|
||||
// +listType=set
|
||||
// +kubebuilder:validation:MinItems=1
|
||||
AllowedScopes []Scope `json:"allowedScopes"`
|
||||
|
||||
// tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient.
|
||||
// +optional
|
||||
TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient.
|
||||
type OIDCClientTokenLifetimes struct {
|
||||
// idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of
|
||||
// ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID
|
||||
// tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used.
|
||||
// This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens
|
||||
// short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the
|
||||
// external identity provider to decide if it is acceptable for the end user to continue their session, and will
|
||||
// update the end user's group memberships from the external identity provider. Giving these tokens a long life is
|
||||
// will allow the end user to continue to use a token while avoiding these updates from the external identity
|
||||
// provider. However, some web applications may have reasons specific to the design of that application to prefer
|
||||
// longer lifetimes.
|
||||
// +kubebuilder:validation:Minimum=120
|
||||
// +kubebuilder:validation:Maximum=1800
|
||||
// +optional
|
||||
IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCClientStatus is a struct that describes the actual state of an OIDCClient.
|
||||
type OIDCClientStatus struct {
|
||||
// phase summarizes the overall status of the OIDCClient.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase OIDCClientPhase `json:"phase,omitempty"`
|
||||
|
||||
// conditions represent the observations of an OIDCClient's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
|
||||
// totalClientSecrets is the current number of client secrets that are detected for this OIDCClient.
|
||||
// +optional
|
||||
TotalClientSecrets int32 `json:"totalClientSecrets"` // do not omitempty to allow it to show in the printer column even when it is 0
|
||||
}
|
||||
|
||||
// OIDCClient describes the configuration of an OIDC client.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped
|
||||
// +kubebuilder:printcolumn:name="Privileged Scopes",type=string,JSONPath=`.spec.allowedScopes[?(@ == "pinniped:request-audience")]`
|
||||
// +kubebuilder:printcolumn:name="Client Secrets",type=integer,JSONPath=`.status.totalClientSecrets`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type OIDCClient struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec of the OIDC client.
|
||||
Spec OIDCClientSpec `json:"spec"`
|
||||
|
||||
// Status of the OIDC client.
|
||||
Status OIDCClientStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of OIDCClient objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCClientList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []OIDCClient `json:"items"`
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=idp.supervisor.pinniped.dev
|
||||
// +groupGoName=IDP
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity provider (IDP) API.
|
||||
package v1alpha1
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "idp.supervisor.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&OIDCIdentityProvider{},
|
||||
&OIDCIdentityProviderList{},
|
||||
&LDAPIdentityProvider{},
|
||||
&LDAPIdentityProviderList{},
|
||||
&ActiveDirectoryIdentityProvider{},
|
||||
&ActiveDirectoryIdentityProviderList{},
|
||||
&GitHubIdentityProvider{},
|
||||
&GitHubIdentityProviderList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type ActiveDirectoryIdentityProviderPhase string
|
||||
|
||||
const (
|
||||
// ActiveDirectoryPhasePending is the default phase for newly-created ActiveDirectoryIdentityProvider resources.
|
||||
ActiveDirectoryPhasePending ActiveDirectoryIdentityProviderPhase = "Pending"
|
||||
|
||||
// ActiveDirectoryPhaseReady is the phase for an ActiveDirectoryIdentityProvider resource in a healthy state.
|
||||
ActiveDirectoryPhaseReady ActiveDirectoryIdentityProviderPhase = "Ready"
|
||||
|
||||
// ActiveDirectoryPhaseError is the phase for an ActiveDirectoryIdentityProvider in an unhealthy state.
|
||||
ActiveDirectoryPhaseError ActiveDirectoryIdentityProviderPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of an Active Directory identity provider.
|
||||
type ActiveDirectoryIdentityProviderStatus struct {
|
||||
// Phase summarizes the overall status of the ActiveDirectoryIdentityProvider.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase ActiveDirectoryIdentityProviderPhase `json:"phase,omitempty"`
|
||||
|
||||
// Represents the observations of an identity provider's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
}
|
||||
|
||||
type ActiveDirectoryIdentityProviderBind struct {
|
||||
// SecretName contains the name of a namespace-local Secret object that provides the username and
|
||||
// password for an Active Directory bind user. This account will be used to perform LDAP searches. The Secret should be
|
||||
// of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value
|
||||
// should be the full dn (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com".
|
||||
// The password must be non-empty.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
SecretName string `json:"secretName"`
|
||||
}
|
||||
|
||||
type ActiveDirectoryIdentityProviderUserSearchAttributes struct {
|
||||
// Username specifies the name of the attribute in Active Directory entry whose value shall become the username
|
||||
// of the user after a successful authentication.
|
||||
// Optional, when empty this defaults to "userPrincipalName".
|
||||
// +optional
|
||||
Username string `json:"username,omitempty"`
|
||||
|
||||
// UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely
|
||||
// identify the user within this ActiveDirectory provider after a successful authentication.
|
||||
// Optional, when empty this defaults to "objectGUID".
|
||||
// +optional
|
||||
UID string `json:"uid,omitempty"`
|
||||
}
|
||||
|
||||
type ActiveDirectoryIdentityProviderGroupSearchAttributes struct {
|
||||
// GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name
|
||||
// in the user's list of groups after a successful authentication.
|
||||
// The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory
|
||||
// server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn".
|
||||
// Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain",
|
||||
// where domain is constructed from the domain components of the group DN.
|
||||
// +optional
|
||||
GroupName string `json:"groupName,omitempty"`
|
||||
}
|
||||
|
||||
type ActiveDirectoryIdentityProviderUserSearch struct {
|
||||
// Base is the dn (distinguished name) that should be used as the search base when searching for users.
|
||||
// E.g. "ou=users,dc=example,dc=com".
|
||||
// Optional, when not specified it will be based on the result of a query for the defaultNamingContext
|
||||
// (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse).
|
||||
// The default behavior searches your entire domain for users.
|
||||
// It may make sense to specify a subtree as a search base if you wish to exclude some users
|
||||
// or to make searches faster.
|
||||
// +optional
|
||||
Base string `json:"base,omitempty"`
|
||||
|
||||
// Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur
|
||||
// in the filter at least once and will be dynamically replaced by the username for which the search is being run.
|
||||
// E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see
|
||||
// https://ldap.com/ldap-filters.
|
||||
// Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used.
|
||||
// Optional. When not specified, the default will be
|
||||
// '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))'
|
||||
// This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account,
|
||||
// and is not shown in advanced view only
|
||||
// (which would likely mean its a system created service account with advanced permissions).
|
||||
// Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username.
|
||||
// +optional
|
||||
Filter string `json:"filter,omitempty"`
|
||||
|
||||
// Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as
|
||||
// the result of the user search.
|
||||
// +optional
|
||||
Attributes ActiveDirectoryIdentityProviderUserSearchAttributes `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type ActiveDirectoryIdentityProviderGroupSearch struct {
|
||||
// Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g.
|
||||
// "ou=groups,dc=example,dc=com".
|
||||
// Optional, when not specified it will be based on the result of a query for the defaultNamingContext
|
||||
// (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse).
|
||||
// The default behavior searches your entire domain for groups.
|
||||
// It may make sense to specify a subtree as a search base if you wish to exclude some groups
|
||||
// for security reasons or to make searches faster.
|
||||
// +optional
|
||||
Base string `json:"base,omitempty"`
|
||||
|
||||
// Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user.
|
||||
// The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the
|
||||
// value of an attribute of the user entry found as a result of the user search. Which attribute's
|
||||
// value is used to replace the placeholder(s) depends on the value of UserAttributeForFilter.
|
||||
// E.g. "member={}" or "&(objectClass=groupOfNames)(member={})".
|
||||
// For more information about ActiveDirectory filters, see https://ldap.com/ldap-filters.
|
||||
// Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used.
|
||||
// Optional. When not specified, the default will act as if the filter were specified as
|
||||
// "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})".
|
||||
// This searches nested groups by default.
|
||||
// Note that nested group search can be slow for some Active Directory servers. To disable it,
|
||||
// you can set the filter to
|
||||
// "(&(objectClass=group)(member={})"
|
||||
// +optional
|
||||
Filter string `json:"filter,omitempty"`
|
||||
|
||||
// UserAttributeForFilter specifies which attribute's value from the user entry found as a result of
|
||||
// the user search will be used to replace the "{}" placeholder(s) in the group search Filter.
|
||||
// For example, specifying "uid" as the UserAttributeForFilter while specifying
|
||||
// "&(objectClass=posixGroup)(memberUid={})" as the Filter would search for groups by replacing
|
||||
// the "{}" placeholder in the Filter with the value of the user's "uid" attribute.
|
||||
// Optional. When not specified, the default will act as if "dn" were specified. For example, leaving
|
||||
// UserAttributeForFilter unspecified while specifying "&(objectClass=groupOfNames)(member={})" as the Filter
|
||||
// would search for groups by replacing the "{}" placeholder(s) with the dn (distinguished name) of the user.
|
||||
// +optional
|
||||
UserAttributeForFilter string `json:"userAttributeForFilter,omitempty"`
|
||||
|
||||
// Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as
|
||||
// the result of the group search.
|
||||
// +optional
|
||||
Attributes ActiveDirectoryIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"`
|
||||
|
||||
// The user's group membership is refreshed as they interact with the supervisor
|
||||
// to obtain new credentials (as their old credentials expire). This allows group
|
||||
// membership changes to be quickly reflected into Kubernetes clusters. Since
|
||||
// group membership is often used to bind authorization policies, it is important
|
||||
// to keep the groups observed in Kubernetes clusters in-sync with the identity
|
||||
// provider.
|
||||
//
|
||||
// In some environments, frequent group membership queries may result in a
|
||||
// significant performance impact on the identity provider and/or the supervisor.
|
||||
// The best approach to handle performance impacts is to tweak the group query
|
||||
// to be more performant, for example by disabling nested group search or by
|
||||
// using a more targeted group search base.
|
||||
//
|
||||
// If the group search query cannot be made performant and you are willing to
|
||||
// have group memberships remain static for approximately a day, then set
|
||||
// skipGroupRefresh to true. This is an insecure configuration as authorization
|
||||
// policies that are bound to group membership will not notice if a user has
|
||||
// been removed from a particular group until their next login.
|
||||
//
|
||||
// This is an experimental feature that may be removed or significantly altered
|
||||
// in the future. Consumers of this configuration should carefully read all
|
||||
// release notes before upgrading to ensure that the meaning of this field has
|
||||
// not changed.
|
||||
SkipGroupRefresh bool `json:"skipGroupRefresh,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring an ActiveDirectory identity provider.
|
||||
type ActiveDirectoryIdentityProviderSpec struct {
|
||||
// Host is the hostname of this Active Directory identity provider, i.e., where to connect. For example: ldap.example.com:636.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Host string `json:"host"`
|
||||
|
||||
// TLS contains the connection settings for how to establish the connection to the Host.
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
|
||||
// Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server
|
||||
// to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt.
|
||||
Bind ActiveDirectoryIdentityProviderBind `json:"bind,omitempty"`
|
||||
|
||||
// UserSearch contains the configuration for searching for a user by name in Active Directory.
|
||||
UserSearch ActiveDirectoryIdentityProviderUserSearch `json:"userSearch,omitempty"`
|
||||
|
||||
// GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory.
|
||||
GroupSearch ActiveDirectoryIdentityProviderGroupSearch `json:"groupSearch,omitempty"`
|
||||
}
|
||||
|
||||
// ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory identity provider.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps
|
||||
// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type ActiveDirectoryIdentityProvider struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec for configuring the identity provider.
|
||||
Spec ActiveDirectoryIdentityProviderSpec `json:"spec"`
|
||||
|
||||
// Status of the identity provider.
|
||||
Status ActiveDirectoryIdentityProviderStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of ActiveDirectoryIdentityProvider objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ActiveDirectoryIdentityProviderList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []ActiveDirectoryIdentityProvider `json:"items"`
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
// Copyright 2024-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type GitHubIdentityProviderPhase string
|
||||
|
||||
const (
|
||||
// GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources.
|
||||
GitHubPhasePending GitHubIdentityProviderPhase = "Pending"
|
||||
|
||||
// GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state.
|
||||
GitHubPhaseReady GitHubIdentityProviderPhase = "Ready"
|
||||
|
||||
// GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state.
|
||||
GitHubPhaseError GitHubIdentityProviderPhase = "Error"
|
||||
)
|
||||
|
||||
type GitHubAllowedAuthOrganizationsPolicy string
|
||||
|
||||
const (
|
||||
// GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity
|
||||
// provider, regardless of their organization membership or lack thereof.
|
||||
GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers"
|
||||
|
||||
// GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in
|
||||
// the listed GitHub organizations are allowed to log in.
|
||||
GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations"
|
||||
)
|
||||
|
||||
// GitHubIdentityProviderStatus is the status of an GitHub identity provider.
|
||||
type GitHubIdentityProviderStatus struct {
|
||||
// Phase summarizes the overall status of the GitHubIdentityProvider.
|
||||
//
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase GitHubIdentityProviderPhase `json:"phase,omitempty"`
|
||||
|
||||
// Conditions represents the observations of an identity provider's current state.
|
||||
//
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
}
|
||||
|
||||
// GitHubAPIConfig allows configuration for GitHub Enterprise Server
|
||||
type GitHubAPIConfig struct {
|
||||
// Host is required only for GitHub Enterprise Server.
|
||||
// Defaults to using GitHub's public API ("github.com").
|
||||
// For convenience, specifying "github.com" is equivalent to specifying "api.github.com".
|
||||
// Do not specify a protocol or scheme since "https://" will always be used.
|
||||
// Port is optional. Do not specify a path, query, fragment, or userinfo.
|
||||
// Only specify domain name or IP address, subdomains (optional), and port (optional).
|
||||
// IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address
|
||||
// in square brackets. Example: "[::1]:443".
|
||||
//
|
||||
// +kubebuilder:default="github.com"
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +optional
|
||||
Host *string `json:"host"`
|
||||
|
||||
// TLS configuration for GitHub Enterprise Server.
|
||||
// Note that this field should not be needed when using GitHub's public API ("github.com").
|
||||
// However, if you choose to specify this field when using GitHub's public API, you must
|
||||
// specify a CA bundle that will verify connections to "api.github.com".
|
||||
//
|
||||
// +optional
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present
|
||||
// to Kubernetes. See the response schema for
|
||||
// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user).
|
||||
type GitHubUsernameAttribute string
|
||||
|
||||
const (
|
||||
// GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes.
|
||||
GitHubUsernameID GitHubUsernameAttribute = "id"
|
||||
|
||||
// GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes.
|
||||
GitHubUsernameLogin GitHubUsernameAttribute = "login"
|
||||
|
||||
// GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the
|
||||
// username to present to Kubernetes, separated by a colon. Example: "my-login:1234"
|
||||
GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id"
|
||||
)
|
||||
|
||||
// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group
|
||||
// names to present to Kubernetes. See the response schema for
|
||||
// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user).
|
||||
type GitHubGroupNameAttribute string
|
||||
|
||||
const (
|
||||
// GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes.
|
||||
GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name"
|
||||
|
||||
// GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes.
|
||||
GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug"
|
||||
)
|
||||
|
||||
// GitHubClaims allows customization of the username and groups claims.
|
||||
type GitHubClaims struct {
|
||||
// Username configures which property of the GitHub user record shall determine the username in Kubernetes.
|
||||
//
|
||||
// Can be either "id", "login", or "login:id". Defaults to "login:id".
|
||||
//
|
||||
// GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens,
|
||||
// and may not start or end with hyphens. GitHub users are allowed to change their login name,
|
||||
// although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar",
|
||||
// then a second user might change their name from "baz" to "foo" in order to take the old
|
||||
// username of the first user. For this reason, it is not as safe to make authorization decisions
|
||||
// based only on the user's login attribute.
|
||||
//
|
||||
// If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's
|
||||
// FederationDomain to further customize how these usernames are presented to Kubernetes.
|
||||
//
|
||||
// Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and
|
||||
// unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value
|
||||
// from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable
|
||||
// choice to concatenate the two values.
|
||||
//
|
||||
// See the response schema for
|
||||
// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user).
|
||||
//
|
||||
// +kubebuilder:default="login:id"
|
||||
// +kubebuilder:validation:Enum={"id","login","login:id"}
|
||||
// +optional
|
||||
Username *GitHubUsernameAttribute `json:"username"`
|
||||
|
||||
// Groups configures which property of the GitHub team record shall determine the group names in Kubernetes.
|
||||
//
|
||||
// Can be either "name" or "slug". Defaults to "slug".
|
||||
//
|
||||
// GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!").
|
||||
//
|
||||
// GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins").
|
||||
//
|
||||
// Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a
|
||||
// forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters
|
||||
// or single hyphens, so the first forward slash `/` will be the separator between the organization login name and
|
||||
// the team name or slug.
|
||||
//
|
||||
// If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's
|
||||
// FederationDomain to further customize how these group names are presented to Kubernetes.
|
||||
//
|
||||
// See the response schema for
|
||||
// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user).
|
||||
//
|
||||
// +kubebuilder:default=slug
|
||||
// +kubebuilder:validation:Enum=name;slug
|
||||
// +optional
|
||||
Groups *GitHubGroupNameAttribute `json:"groups"`
|
||||
}
|
||||
|
||||
// GitHubClientSpec contains information about the GitHub client that this identity provider will use
|
||||
// for web-based login flows.
|
||||
type GitHubClientSpec struct {
|
||||
// SecretName contains the name of a namespace-local Secret object that provides the clientID and
|
||||
// clientSecret for an GitHub App or GitHub OAuth2 client.
|
||||
//
|
||||
// This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret".
|
||||
//
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
SecretName string `json:"secretName"`
|
||||
}
|
||||
|
||||
type GitHubOrganizationsSpec struct {
|
||||
// Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers".
|
||||
// Defaults to "OnlyUsersFromAllowedOrganizations".
|
||||
//
|
||||
// Must be set to "AllGitHubUsers" if the allowed field is empty.
|
||||
//
|
||||
// This field only exists to ensure that Pinniped administrators are aware that an empty list of
|
||||
// allowedOrganizations means all GitHub users are allowed to log in.
|
||||
//
|
||||
// +kubebuilder:default=OnlyUsersFromAllowedOrganizations
|
||||
// +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers
|
||||
// +optional
|
||||
Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"`
|
||||
|
||||
// Allowed, when specified, indicates that only users with membership in at least one of the listed
|
||||
// GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include
|
||||
// teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be
|
||||
// provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP.
|
||||
//
|
||||
// The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations,
|
||||
// otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams
|
||||
// within that organization.
|
||||
//
|
||||
// If no organizations are listed, you must set organizations: AllGitHubUsers.
|
||||
//
|
||||
// +kubebuilder:validation:MaxItems=64
|
||||
// +listType=set
|
||||
// +optional
|
||||
Allowed []string `json:"allowed,omitempty"`
|
||||
}
|
||||
|
||||
// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how.
|
||||
type GitHubAllowAuthenticationSpec struct {
|
||||
// Organizations allows customization of which organizations can authenticate using this IDP.
|
||||
// +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')"
|
||||
// +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')"
|
||||
Organizations GitHubOrganizationsSpec `json:"organizations"`
|
||||
}
|
||||
|
||||
// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider.
|
||||
type GitHubIdentityProviderSpec struct {
|
||||
// GitHubAPI allows configuration for GitHub Enterprise Server
|
||||
//
|
||||
// +kubebuilder:default={}
|
||||
GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"`
|
||||
|
||||
// Claims allows customization of the username and groups claims.
|
||||
//
|
||||
// +kubebuilder:default={}
|
||||
Claims GitHubClaims `json:"claims,omitempty"`
|
||||
|
||||
// AllowAuthentication allows customization of who can authenticate using this IDP and how.
|
||||
AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"`
|
||||
|
||||
// Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client).
|
||||
Client GitHubClientSpec `json:"client"`
|
||||
}
|
||||
|
||||
// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider.
|
||||
// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App.
|
||||
//
|
||||
// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured
|
||||
// as OIDCClients.
|
||||
//
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps
|
||||
// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type GitHubIdentityProvider struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec for configuring the identity provider.
|
||||
Spec GitHubIdentityProviderSpec `json:"spec"`
|
||||
|
||||
// Status of the identity provider.
|
||||
Status GitHubIdentityProviderStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// GitHubIdentityProviderList lists GitHubIdentityProvider objects.
|
||||
//
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type GitHubIdentityProviderList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []GitHubIdentityProvider `json:"items"`
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type LDAPIdentityProviderPhase string
|
||||
|
||||
const (
|
||||
// LDAPPhasePending is the default phase for newly-created LDAPIdentityProvider resources.
|
||||
LDAPPhasePending LDAPIdentityProviderPhase = "Pending"
|
||||
|
||||
// LDAPPhaseReady is the phase for an LDAPIdentityProvider resource in a healthy state.
|
||||
LDAPPhaseReady LDAPIdentityProviderPhase = "Ready"
|
||||
|
||||
// LDAPPhaseError is the phase for an LDAPIdentityProvider in an unhealthy state.
|
||||
LDAPPhaseError LDAPIdentityProviderPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of an LDAP identity provider.
|
||||
type LDAPIdentityProviderStatus struct {
|
||||
// Phase summarizes the overall status of the LDAPIdentityProvider.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase LDAPIdentityProviderPhase `json:"phase,omitempty"`
|
||||
|
||||
// Represents the observations of an identity provider's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
}
|
||||
|
||||
type LDAPIdentityProviderBind struct {
|
||||
// SecretName contains the name of a namespace-local Secret object that provides the username and
|
||||
// password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be
|
||||
// of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value
|
||||
// should be the full dn (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com".
|
||||
// The password must be non-empty.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
SecretName string `json:"secretName"`
|
||||
}
|
||||
|
||||
type LDAPIdentityProviderUserSearchAttributes struct {
|
||||
// Username specifies the name of the attribute in the LDAP entry whose value shall become the username
|
||||
// of the user after a successful authentication. This would typically be the same attribute name used in
|
||||
// the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName".
|
||||
// The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP
|
||||
// server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field
|
||||
// is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default
|
||||
// value of "dn={}" would not work.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Username string `json:"username,omitempty"`
|
||||
|
||||
// UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely
|
||||
// identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID".
|
||||
// The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP
|
||||
// server in the user's entry. Distinguished names can be used by specifying lower-case "dn".
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
UID string `json:"uid,omitempty"`
|
||||
}
|
||||
|
||||
type LDAPIdentityProviderGroupSearchAttributes struct {
|
||||
// GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name
|
||||
// in the user's list of groups after a successful authentication.
|
||||
// The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP
|
||||
// server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn".
|
||||
// Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name).
|
||||
// +optional
|
||||
GroupName string `json:"groupName,omitempty"`
|
||||
}
|
||||
|
||||
type LDAPIdentityProviderUserSearch struct {
|
||||
// Base is the dn (distinguished name) that should be used as the search base when searching for users.
|
||||
// E.g. "ou=users,dc=example,dc=com".
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Base string `json:"base,omitempty"`
|
||||
|
||||
// Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur
|
||||
// in the filter at least once and will be dynamically replaced by the username for which the search is being run.
|
||||
// E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see
|
||||
// https://ldap.com/ldap-filters.
|
||||
// Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used.
|
||||
// Optional. When not specified, the default will act as if the Filter were specified as the value from
|
||||
// Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be
|
||||
// explicitly specified, since the default value of "dn={}" would not work.
|
||||
// +optional
|
||||
Filter string `json:"filter,omitempty"`
|
||||
|
||||
// Attributes specifies how the user's information should be read from the LDAP entry which was found as
|
||||
// the result of the user search.
|
||||
// +optional
|
||||
Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type LDAPIdentityProviderGroupSearch struct {
|
||||
// Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g.
|
||||
// "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and
|
||||
// authenticated users will not belong to any groups from the LDAP provider. Also, when not specified,
|
||||
// the values of Filter, UserAttributeForFilter, Attributes, and SkipGroupRefresh are ignored.
|
||||
// +optional
|
||||
Base string `json:"base,omitempty"`
|
||||
|
||||
// Filter is the LDAP search filter which should be applied when searching for groups for a user.
|
||||
// The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the
|
||||
// value of an attribute of the user entry found as a result of the user search. Which attribute's
|
||||
// value is used to replace the placeholder(s) depends on the value of UserAttributeForFilter.
|
||||
// For more information about LDAP filters, see https://ldap.com/ldap-filters.
|
||||
// Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used.
|
||||
// Optional. When not specified, the default will act as if the Filter were specified as "member={}".
|
||||
// +optional
|
||||
Filter string `json:"filter,omitempty"`
|
||||
|
||||
// UserAttributeForFilter specifies which attribute's value from the user entry found as a result of
|
||||
// the user search will be used to replace the "{}" placeholder(s) in the group search Filter.
|
||||
// For example, specifying "uid" as the UserAttributeForFilter while specifying
|
||||
// "&(objectClass=posixGroup)(memberUid={})" as the Filter would search for groups by replacing
|
||||
// the "{}" placeholder in the Filter with the value of the user's "uid" attribute.
|
||||
// Optional. When not specified, the default will act as if "dn" were specified. For example, leaving
|
||||
// UserAttributeForFilter unspecified while specifying "&(objectClass=groupOfNames)(member={})" as the Filter
|
||||
// would search for groups by replacing the "{}" placeholder(s) with the dn (distinguished name) of the user.
|
||||
// +optional
|
||||
UserAttributeForFilter string `json:"userAttributeForFilter,omitempty"`
|
||||
|
||||
// Attributes specifies how the group's information should be read from each LDAP entry which was found as
|
||||
// the result of the group search.
|
||||
// +optional
|
||||
Attributes LDAPIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"`
|
||||
|
||||
// The user's group membership is refreshed as they interact with the supervisor
|
||||
// to obtain new credentials (as their old credentials expire). This allows group
|
||||
// membership changes to be quickly reflected into Kubernetes clusters. Since
|
||||
// group membership is often used to bind authorization policies, it is important
|
||||
// to keep the groups observed in Kubernetes clusters in-sync with the identity
|
||||
// provider.
|
||||
//
|
||||
// In some environments, frequent group membership queries may result in a
|
||||
// significant performance impact on the identity provider and/or the supervisor.
|
||||
// The best approach to handle performance impacts is to tweak the group query
|
||||
// to be more performant, for example by disabling nested group search or by
|
||||
// using a more targeted group search base.
|
||||
//
|
||||
// If the group search query cannot be made performant and you are willing to
|
||||
// have group memberships remain static for approximately a day, then set
|
||||
// skipGroupRefresh to true. This is an insecure configuration as authorization
|
||||
// policies that are bound to group membership will not notice if a user has
|
||||
// been removed from a particular group until their next login.
|
||||
//
|
||||
// This is an experimental feature that may be removed or significantly altered
|
||||
// in the future. Consumers of this configuration should carefully read all
|
||||
// release notes before upgrading to ensure that the meaning of this field has
|
||||
// not changed.
|
||||
SkipGroupRefresh bool `json:"skipGroupRefresh,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring an LDAP identity provider.
|
||||
type LDAPIdentityProviderSpec struct {
|
||||
// Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Host string `json:"host"`
|
||||
|
||||
// TLS contains the connection settings for how to establish the connection to the Host.
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
|
||||
// Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server
|
||||
// to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt.
|
||||
Bind LDAPIdentityProviderBind `json:"bind,omitempty"`
|
||||
|
||||
// UserSearch contains the configuration for searching for a user by name in the LDAP provider.
|
||||
UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"`
|
||||
|
||||
// GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider.
|
||||
GroupSearch LDAPIdentityProviderGroupSearch `json:"groupSearch,omitempty"`
|
||||
}
|
||||
|
||||
// LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access
|
||||
// Protocol (LDAP) identity provider.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps
|
||||
// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type LDAPIdentityProvider struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec for configuring the identity provider.
|
||||
Spec LDAPIdentityProviderSpec `json:"spec"`
|
||||
|
||||
// Status of the identity provider.
|
||||
Status LDAPIdentityProviderStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of LDAPIdentityProvider objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type LDAPIdentityProviderList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []LDAPIdentityProvider `json:"items"`
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type OIDCIdentityProviderPhase string
|
||||
|
||||
const (
|
||||
// PhasePending is the default phase for newly-created OIDCIdentityProvider resources.
|
||||
PhasePending OIDCIdentityProviderPhase = "Pending"
|
||||
|
||||
// PhaseReady is the phase for an OIDCIdentityProvider resource in a healthy state.
|
||||
PhaseReady OIDCIdentityProviderPhase = "Ready"
|
||||
|
||||
// PhaseError is the phase for an OIDCIdentityProvider in an unhealthy state.
|
||||
PhaseError OIDCIdentityProviderPhase = "Error"
|
||||
)
|
||||
|
||||
// OIDCIdentityProviderStatus is the status of an OIDC identity provider.
|
||||
type OIDCIdentityProviderStatus struct {
|
||||
// Phase summarizes the overall status of the OIDCIdentityProvider.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase OIDCIdentityProviderPhase `json:"phase,omitempty"`
|
||||
|
||||
// Represents the observations of an identity provider's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
}
|
||||
|
||||
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
|
||||
// request parameters.
|
||||
type OIDCAuthorizationConfig struct {
|
||||
// additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization
|
||||
// request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials
|
||||
// Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is
|
||||
// always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request
|
||||
// the following scopes: "openid", "offline_access", "email", and "profile". See
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email"
|
||||
// scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the
|
||||
// "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves,
|
||||
// or as common patterns used by providers who implement the standard in the ecosystem evolve.
|
||||
// By setting this list to anything other than an empty list, you are overriding the
|
||||
// default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list.
|
||||
// If you do not want any of these scopes to be requested, you may set this list to contain only "openid".
|
||||
// Some OIDC providers may also require a scope to get access to the user's group membership, in which case you
|
||||
// may wish to include it in this list. Sometimes the scope to request the user's group membership is called
|
||||
// "groups", but unfortunately this is not specified in the OIDC standard.
|
||||
// Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by
|
||||
// your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in
|
||||
// the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See
|
||||
// your OIDC provider's documentation for more information about what scopes are available to request claims.
|
||||
// Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor
|
||||
// from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be
|
||||
// "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its
|
||||
// requirements for what to include in the request in order to receive a refresh token in the response, if anything.
|
||||
// Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider
|
||||
// may ignore scopes that it does not understand or require (see
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the
|
||||
// "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC
|
||||
// provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
|
||||
// +optional
|
||||
AdditionalScopes []string `json:"additionalScopes,omitempty"`
|
||||
|
||||
// additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your
|
||||
// OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra
|
||||
// parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id",
|
||||
// "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be
|
||||
// included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time.
|
||||
// The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user
|
||||
// should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting
|
||||
// ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped.
|
||||
// This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password
|
||||
// Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the
|
||||
// Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt"
|
||||
// parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's
|
||||
// authorization endpoint for its requirements for what to include in the request in order to receive a refresh
|
||||
// token in the response, if anything. If your provider requires the prompt parameter to request a refresh token,
|
||||
// then include it here. Also note that most providers also require a certain scope to be requested in order to
|
||||
// receive refresh tokens. See the additionalScopes setting for more information about using scopes to request
|
||||
// refresh tokens.
|
||||
// +optional
|
||||
// +patchMergeKey=name
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=name
|
||||
AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"`
|
||||
|
||||
// allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant
|
||||
// (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a
|
||||
// username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow.
|
||||
// The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be
|
||||
// supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password
|
||||
// Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose
|
||||
// to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the
|
||||
// cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be
|
||||
// convenient for users, especially for identities from your OIDC provider which are not intended to represent a human
|
||||
// actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it,
|
||||
// you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this
|
||||
// OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password
|
||||
// Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords
|
||||
// (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other
|
||||
// web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins.
|
||||
// allowPasswordGrant defaults to false.
|
||||
// +optional
|
||||
AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"`
|
||||
}
|
||||
|
||||
// Parameter is a key/value pair which represents a parameter in an HTTP request.
|
||||
type Parameter struct {
|
||||
// The name of the parameter. Required.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Name string `json:"name"`
|
||||
|
||||
// The value of the parameter.
|
||||
// +optional
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCClaims provides a mapping from upstream claims into identities.
|
||||
type OIDCClaims struct {
|
||||
// Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain
|
||||
// the groups to which an identity belongs. By default, the identities will not include any group memberships when
|
||||
// this setting is not configured.
|
||||
// +optional
|
||||
Groups string `json:"groups"`
|
||||
|
||||
// Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to
|
||||
// ascertain an identity's username. When not set, the username will be an automatically constructed unique string
|
||||
// which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from
|
||||
// the ID token.
|
||||
// +optional
|
||||
Username string `json:"username"`
|
||||
|
||||
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
|
||||
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
|
||||
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
|
||||
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
|
||||
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
|
||||
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
|
||||
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
|
||||
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|
||||
// +optional
|
||||
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCClient contains information about an OIDC client (e.g., client ID and client
|
||||
// secret).
|
||||
type OIDCClient struct {
|
||||
// SecretName contains the name of a namespace-local Secret object that provides the clientID and
|
||||
// clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient
|
||||
// struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys
|
||||
// "clientID" and "clientSecret".
|
||||
SecretName string `json:"secretName"`
|
||||
}
|
||||
|
||||
// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
|
||||
type OIDCIdentityProviderSpec struct {
|
||||
// Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
|
||||
// /.well-known/openid-configuration.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://`
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// TLS configuration for discovery/JWKS requests to the issuer.
|
||||
// +optional
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
|
||||
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
||||
// parameters to be used with this OIDC identity provider.
|
||||
// +optional
|
||||
AuthorizationConfig OIDCAuthorizationConfig `json:"authorizationConfig,omitempty"`
|
||||
|
||||
// Claims provides the names of token claims that will be used when inspecting an identity from
|
||||
// this OIDC identity provider.
|
||||
// +optional
|
||||
Claims OIDCClaims `json:"claims"`
|
||||
|
||||
// OIDCClient contains OIDC client information to be used used with this OIDC identity
|
||||
// provider.
|
||||
Client OIDCClient `json:"client"`
|
||||
}
|
||||
|
||||
// OIDCIdentityProvider describes the configuration of an upstream OpenID Connect identity provider.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps
|
||||
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type OIDCIdentityProvider struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec for configuring the identity provider.
|
||||
Spec OIDCIdentityProviderSpec `json:"spec"`
|
||||
|
||||
// Status of the identity provider.
|
||||
Status OIDCIdentityProviderStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCIdentityProviderList lists OIDCIdentityProvider objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCIdentityProviderList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []OIDCIdentityProvider `json:"items"`
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles.
|
||||
//
|
||||
// +kubebuilder:validation:Enum=Secret;ConfigMap
|
||||
type CertificateAuthorityDataSourceKind string
|
||||
|
||||
const (
|
||||
// CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles.
|
||||
CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap")
|
||||
|
||||
// CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles.
|
||||
// Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque.
|
||||
CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret")
|
||||
)
|
||||
|
||||
// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification.
|
||||
type CertificateAuthorityDataSourceSpec struct {
|
||||
// Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
|
||||
// Allowed values are "Secret" or "ConfigMap".
|
||||
// "ConfigMap" uses a Kubernetes configmap to source CA Bundles.
|
||||
// "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
|
||||
Kind CertificateAuthorityDataSourceKind `json:"kind"`
|
||||
// Name is the resource name of the secret or configmap from which to read the CA bundle.
|
||||
// The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Name string `json:"name"`
|
||||
// Key is the key name within the secret or configmap from which to read the CA bundle.
|
||||
// The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
|
||||
// certificate bundle.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// TLSSpec provides TLS configuration for identity provider integration.
|
||||
type TLSSpec struct {
|
||||
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||
// +optional
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
|
||||
// Reference to a CA bundle in a secret or a configmap.
|
||||
// Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
|
||||
// +optional
|
||||
CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"`
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// IDPType are the strings that can be returned by the Supervisor identity provider discovery endpoint
|
||||
// as the "type" of each returned identity provider.
|
||||
type IDPType string
|
||||
|
||||
// IDPFlow are the strings that can be returned by the Supervisor identity provider discovery endpoint
|
||||
// in the array of allowed client "flows" for each returned identity provider.
|
||||
type IDPFlow string
|
||||
|
||||
const (
|
||||
IDPTypeOIDC IDPType = "oidc"
|
||||
IDPTypeLDAP IDPType = "ldap"
|
||||
IDPTypeActiveDirectory IDPType = "activedirectory"
|
||||
IDPTypeGitHub IDPType = "github"
|
||||
|
||||
IDPFlowCLIPassword IDPFlow = "cli_password"
|
||||
IDPFlowBrowserAuthcode IDPFlow = "browser_authcode"
|
||||
)
|
||||
|
||||
// Equals is a convenience function for comparing an IDPType to a string.
|
||||
func (r IDPType) Equals(s string) bool {
|
||||
return string(r) == s
|
||||
}
|
||||
|
||||
// String is a convenience function to convert an IDPType to a string.
|
||||
func (r IDPType) String() string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// Equals is a convenience function for comparing an IDPFlow to a string.
|
||||
func (r IDPFlow) Equals(s string) bool {
|
||||
return string(r) == s
|
||||
}
|
||||
|
||||
// String is a convenience function to convert an IDPFlow to a string.
|
||||
func (r IDPFlow) String() string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// OIDCDiscoveryResponse is part of the response from a FederationDomain's OpenID Provider Configuration
|
||||
// Document returned by the .well-known/openid-configuration endpoint. It ignores all the standard OpenID Provider
|
||||
// configuration metadata and only picks out the portion related to Supervisor identity provider discovery.
|
||||
type OIDCDiscoveryResponse struct {
|
||||
SupervisorDiscovery OIDCDiscoveryResponseIDPEndpoint `json:"discovery.supervisor.pinniped.dev/v1alpha1"`
|
||||
}
|
||||
|
||||
// OIDCDiscoveryResponseIDPEndpoint contains the URL for the identity provider discovery endpoint.
|
||||
type OIDCDiscoveryResponseIDPEndpoint struct {
|
||||
PinnipedIDPsEndpoint string `json:"pinniped_identity_providers_endpoint"`
|
||||
}
|
||||
|
||||
// IDPDiscoveryResponse is the response of a FederationDomain's identity provider discovery endpoint.
|
||||
type IDPDiscoveryResponse struct {
|
||||
PinnipedIDPs []PinnipedIDP `json:"pinniped_identity_providers"`
|
||||
PinnipedSupportedIDPTypes []PinnipedSupportedIDPType `json:"pinniped_supported_identity_provider_types"`
|
||||
}
|
||||
|
||||
// PinnipedIDP describes a single identity provider as included in the response of a FederationDomain's
|
||||
// identity provider discovery endpoint.
|
||||
type PinnipedIDP struct {
|
||||
Name string `json:"name"`
|
||||
Type IDPType `json:"type"`
|
||||
Flows []IDPFlow `json:"flows,omitempty"`
|
||||
}
|
||||
|
||||
// PinnipedSupportedIDPType describes a single identity provider type.
|
||||
type PinnipedSupportedIDPType struct {
|
||||
Type IDPType `json:"type"`
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package oidc
|
||||
|
||||
// Constants related to the Supervisor FederationDomain's authorization and token endpoints.
|
||||
const (
|
||||
// AuthorizeUsernameHeaderName is the name of the HTTP header which can be used to transmit a username
|
||||
// to the authorize endpoint when using a password flow, for example an OIDCIdentityProvider with a password grant
|
||||
// or an LDAPIdentityProvider.
|
||||
AuthorizeUsernameHeaderName = "Pinniped-Username"
|
||||
|
||||
// AuthorizePasswordHeaderName is the name of the HTTP header which can be used to transmit a password
|
||||
// to the authorize endpoint when using a password flow, for example an OIDCIdentityProvider with a password grant
|
||||
// or an LDAPIdentityProvider.
|
||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||
|
||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||
|
||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||
|
||||
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||
IDTokenClaimIssuer = "iss"
|
||||
|
||||
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||
IDTokenClaimSubject = "sub"
|
||||
|
||||
// IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim
|
||||
// in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated.
|
||||
IDTokenSubClaimIDPNameQueryParam = "idpName"
|
||||
|
||||
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||
IDTokenClaimAuthorizedParty = "azp"
|
||||
|
||||
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||
// username which was mapped from the upstream identity provider.
|
||||
IDTokenClaimUsername = "username"
|
||||
|
||||
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||
// group names which were mapped from the upstream identity provider.
|
||||
IDTokenClaimGroups = "groups"
|
||||
|
||||
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
|
||||
// token, if any claims are present.
|
||||
IDTokenClaimAdditionalClaims = "additionalClaims"
|
||||
|
||||
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||
GrantTypeAuthorizationCode = "authorization_code"
|
||||
|
||||
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||
GrantTypeRefreshToken = "refresh_token"
|
||||
|
||||
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||
|
||||
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||
ScopeOpenID = "openid"
|
||||
|
||||
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||
// tokens.
|
||||
ScopeOfflineAccess = "offline_access"
|
||||
|
||||
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||
ScopeEmail = "email"
|
||||
|
||||
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||
ScopeProfile = "profile"
|
||||
|
||||
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||
// ID tokens.
|
||||
ScopeUsername = "username"
|
||||
|
||||
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||
// ID tokens.
|
||||
ScopeGroups = "groups"
|
||||
|
||||
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||
// be used to request a different audience.
|
||||
ScopeRequestAudience = "pinniped:request-audience"
|
||||
|
||||
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||
ClientIDPinnipedCLI = "pinniped-cli"
|
||||
|
||||
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||
)
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package main is the combined entrypoint for the Pinniped "kube-cert-agent" component.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
// This side effect import ensures that we use fipsonly crypto during TLS in fips_strict mode.
|
||||
//
|
||||
// Commenting this out because it causes the runtime memory consumption of this binary to increase
|
||||
// from ~1 MB to ~8 MB (as measured when running the sleep subcommand). This binary does not use TLS,
|
||||
// so it should not be needed. If this binary is ever changed to make use of TLS client and/or server
|
||||
// code, then we should bring this import back to support the use of the ptls library for client and
|
||||
// server code, and we should also increase the memory limits on the kube cert agent deployment (as
|
||||
// decided by the kube cert agent controller in the Concierge).
|
||||
//
|
||||
//nolint:godot // This is not sentence, it is a commented out line of import code.
|
||||
// _ "go.pinniped.dev/internal/crypto/ptls"
|
||||
|
||||
// This side effect imports cgo so that runtime/cgo gets linked, when in fips_strict mode.
|
||||
// Without this line, the binary will exit 133 upon startup in fips_strict mode.
|
||||
// It also enables fipsonly tls mode, just to be absolutely sure that the fips code is enabled,
|
||||
// even though it shouldn't be used currently by this binary.
|
||||
_ "go.pinniped.dev/internal/crypto/fips"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals // these are swapped during unit tests.
|
||||
var (
|
||||
getenv = os.Getenv
|
||||
fail = log.Fatalf
|
||||
sleep = time.Sleep
|
||||
out = io.Writer(os.Stdout)
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fail("missing subcommand")
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "sleep":
|
||||
sleep(math.MaxInt64)
|
||||
case "print":
|
||||
certBytes, err := os.ReadFile(getenv("CERT_PATH"))
|
||||
if err != nil {
|
||||
fail("could not read CERT_PATH: %v", err)
|
||||
}
|
||||
keyBytes, err := os.ReadFile(getenv("KEY_PATH"))
|
||||
if err != nil {
|
||||
fail("could not read KEY_PATH: %v", err)
|
||||
}
|
||||
if err := json.NewEncoder(out).Encode(&struct {
|
||||
Cert string `json:"tls.crt"`
|
||||
Key string `json:"tls.key"`
|
||||
}{
|
||||
Cert: base64.StdEncoding.EncodeToString(certBytes),
|
||||
Key: base64.StdEncoding.EncodeToString(keyBytes),
|
||||
}); err != nil {
|
||||
fail("failed to write output: %v", err)
|
||||
}
|
||||
default:
|
||||
fail("invalid subcommand %q", os.Args[1])
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type errWriter struct{}
|
||||
|
||||
func (e errWriter) Write([]byte) (int, error) { return 0, fmt.Errorf("some write error") }
|
||||
|
||||
func TestEntrypoint(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
args []string
|
||||
env map[string]string
|
||||
failOutput bool
|
||||
wantSleep time.Duration
|
||||
wantLog string
|
||||
wantOutJSON string
|
||||
wantFail bool
|
||||
}{
|
||||
{
|
||||
name: "missing args",
|
||||
args: []string{},
|
||||
wantLog: "missing subcommand\n",
|
||||
wantFail: true,
|
||||
},
|
||||
{
|
||||
name: "invalid subcommand",
|
||||
args: []string{"/path/to/binary", "invalid"},
|
||||
wantLog: "invalid subcommand \"invalid\"\n",
|
||||
wantFail: true,
|
||||
},
|
||||
{
|
||||
name: "valid sleep",
|
||||
args: []string{"/path/to/binary", "sleep"},
|
||||
wantSleep: 2562047*time.Hour + 47*time.Minute + 16*time.Second + 854775807*time.Nanosecond, // math.MaxInt64 nanoseconds, approximately 290 years
|
||||
},
|
||||
{
|
||||
name: "missing cert file",
|
||||
args: []string{"/path/to/binary", "print"},
|
||||
env: map[string]string{
|
||||
"CERT_PATH": "./does/not/exist",
|
||||
"KEY_PATH": "./testdata/test.key",
|
||||
},
|
||||
wantFail: true,
|
||||
wantLog: "could not read CERT_PATH: open ./does/not/exist: no such file or directory\n",
|
||||
},
|
||||
{
|
||||
name: "missing key file",
|
||||
args: []string{"/path/to/binary", "print"},
|
||||
env: map[string]string{
|
||||
"CERT_PATH": "./testdata/test.crt",
|
||||
"KEY_PATH": "./does/not/exist",
|
||||
},
|
||||
wantFail: true,
|
||||
wantLog: "could not read KEY_PATH: open ./does/not/exist: no such file or directory\n",
|
||||
},
|
||||
{
|
||||
name: "fail to write output",
|
||||
args: []string{"/path/to/binary", "print"},
|
||||
env: map[string]string{
|
||||
"CERT_PATH": "./testdata/test.crt",
|
||||
"KEY_PATH": "./testdata/test.key",
|
||||
},
|
||||
failOutput: true,
|
||||
wantFail: true,
|
||||
wantLog: "failed to write output: some write error\n",
|
||||
},
|
||||
{
|
||||
name: "successful print",
|
||||
args: []string{"/path/to/binary", "print"},
|
||||
env: map[string]string{
|
||||
"CERT_PATH": "./testdata/test.crt",
|
||||
"KEY_PATH": "./testdata/test.key",
|
||||
},
|
||||
wantOutJSON: `{
|
||||
"tls.crt": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJd01EY3lOVEl4TURReE9Gb1hEVE13TURjeU16SXhNRFF4T0Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTDNLCmhZdjJnSVExRHd6aDJjV01pZCtvZkFudkxJZlYyWHY2MXZUTEdwclVJK1hVcUI0L2d0ZjZYNlVObjBMZXR0Mm4KZDhwNHd5N2h3NzNoVS9nZ2R2bVdKdnFCclNqYzNKR2Z5K2tqNjZmS1hYK1BUbGJMN1Fid2lSdmNTcUlYSVdsVgpsSEh4RUNXckVEOGpDdWx3L05WcWZvb2svaDVpTlVDVDl5c3dTSnIvMGZJbWlWbm9UbElvRVlHMmVDTmVqWjVjCmczOXVEM1pUcWQ5WnhXd1NMTG5JKzJrcEpuWkJQY2QxWlE4QVFxekRnWnRZUkNxYWNuNWdja1FVS1pXS1FseG8KRWZ0NmcxWEhKb3VBV0FadzdoRXRrMHY4ckcwL2VLRjd3YW14Rmk2QkZWbGJqV0JzQjRUOXJBcGJkQldUS2VDSgpIdjhmdjVSTUZTenBUM3V6VE84Q0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFDaDVSaGJ4cUplK1ovZ2MxN2NaaEtObWRpd3UKSTJwTHAzUUJmd3ZOK1dibWFqencvN3JZaFkwZDhKWVZUSnpYU0NQV2k2VUFLeEF0WE9MRjhXSUlmOWkzOW42Ugp1S09CR1cxNEZ6ekd5UkppRDNxYUcvSlR2RVcrU0xod2w2OE5kcjVMSFNuYnVnQXFxMzFhYmNReTZabDl2NUE4CkpLQzk3TGovU244cmo3b3BLeTRXM29xN05DUXNBYjB6aDRJbGxSRjZVdlNuSnlTZnNnN3hkWEhIcHhZREh0T1MKWGNPdTV5U1VJWlRnRmU5UmZlVVpsR1o1eG4wY2tNbFE3cVcyV3gxcTBPVld3NXVzNE50a0dxS3JIRzRUbjFYNwp1d28vWXl0bjVzRHhyRHYxL29paTZBWk9Dc1RQcmU0b0Qzd3o0bm1WekNWSmNncnFINFEyNGhUOFdOZz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
|
||||
"tls.key": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBdmNxRmkvYUFoRFVQRE9IWnhZeUozNmg4Q2U4c2g5WFplL3JXOU1zYW10UWo1ZFNvCkhqK0MxL3BmcFEyZlF0NjIzYWQzeW5qREx1SER2ZUZUK0NCMitaWW0rb0d0S056Y2taL0w2U1BycDhwZGY0OU8KVnN2dEJ2Q0pHOXhLb2hjaGFWV1VjZkVRSmFzUVB5TUs2WEQ4MVdwK2lpVCtIbUkxUUpQM0t6Qkltdi9SOGlhSgpXZWhPVWlnUmdiWjRJMTZObmx5RGYyNFBkbE9wMzFuRmJCSXN1Y2o3YVNrbWRrRTl4M1ZsRHdCQ3JNT0JtMWhFCktwcHlmbUJ5UkJRcGxZcENYR2dSKzNxRFZjY21pNEJZQm5EdUVTMlRTL3lzYlQ5NG9YdkJxYkVXTG9FVldWdU4KWUd3SGhQMnNDbHQwRlpNcDRJa2UveCsvbEV3VkxPbFBlN05NN3dJREFRQUJBb0lCQUZDMXRVRW1ITlVjTTBCSgpNM0Q5S1F6Qis2M0YxbXdWbHgxUU9PVjFFZVZSM2NvNU94MVI2UFNyOXN5Y0ZHUTlqZ3FJMHpwNVRKZTlUcDZMCkdraGtsZlBoMU1Xbks5bzZ3bG56V0tYV3JycDJKbmkrbXBQeXVPUEFtcTRNYW5pdjJYZVArMGJST3dxcHlvanYKQUE3eUM3TStUSDIyNlpKR05WczNFVjkrY3dIbWwweXV6QmZJSm4vcnYvdzJnK1dSS00vTUMwUzdrMmQ4YlJsQQpOeWNLVkdBR0JoS1RsdGpvVllPZWg2YUhFcFNqSzh6ZmFlUGpvNWRZSnZvVklsaTYwWUNnY0pPVS84alhUK05wCjFGbTd0UnZBdGozcFVwMFNxZGFmMlJVemg5amZKcDJWRkNIdVNKNlRQcUFyT3lRb2p0TWNUSEYwVGlXN3hySFAKeE9DUklBRUNnWUVBd0dCUFU3dmR0aE1KQmcrT1JVb0dRUWFJdFRlSnZRd0lxSnZiS0Qyb3NwNGpoUzFkR1pCdwpXMzBHS0VjL2dkOEpOdE9xOUJCbk1pY1BGN2hrdHV5K2JTUHY0MVhQdWQ2N3JTU083VHN3MjBDMTBnRlJxMDZCCnpJSldGQVVxSzNJa3ZWYzNWRG10U0xTRG94NFFaL0JkcWFNbFE1eTVKQ3NDNWtUaG1rWkZsTzhDZ1lFQS9JOVgKWUhpNlJpb01KRTFmcU9ISkw0RERqbGV6bWN1UnJEN2ZFNUluS2J0SloySmhHWU9YL0MwS1huSFRPV1RDRHh4TgpGQnZwdkQ2WHY1bzNQaEI5WjZrMmZxdko0R1M4dXJrRy9LVTR4Y0MrYmFrKzlhdmE4b2FpU3FHMTZ6RDlOSDJQCmpKNjBOcmJMbDFKMHBVOWZpd3VGVlVLSjRoRFpPZk45UnFZZHlBRUNnWUFWd284V2hKaUdnTTZ6ZmN6MDczT1gKcFZxUFRQSHFqVkxwWjMrNXBJZlJkR3ZHSTZSMVFNNUV1dmFZVmI3TVBPTTQ3V1pYNXdjVk9DL1AyZzZpVmxNUAoyMUhHSUMyMzg0YTlCZmFZeE9vNDBxLytTaUhudzZDUTlta3dLSWxsa3Fxdk5BOVJHcGtNTVViMmkyOEZvcjJsCmM0dkNneGE2RFpkdFhuczZUUnFQeHdLQmdDZlk1Y3hPdi9UNkJWaGs3TWJVZU0ySjMxREIvWkF5VWhWL0Jlc3MKa0FsQmgxOU1ZazJJT1o2TDdLcmlBcFYzbERhV0hJTWp0RWtEQnlZdnlxOThJbzBNWVpDeXdmTXBjYTEwSytvSQpsMkI3L0krSXVHcENaeFVFc081ZGZUcFNUR0RQdnFwTkQ5bmlGVlVXcVZpN29UTnE2ZXA5eVF0bDVTQURqcXhxCjRTQUJBb0dBSW0waFVnMXd0Y1M0NmNHTHk2UElrUE01dG9jVFNnaHR6NHZGc3VrL2k0UUE5R0JvQk8yZ0g2dHkKK2tKSG1lYVh0MmRtZ3lTcDBRQVdpdDVVbGNlRXVtQjBOWG5BZEpaUXhlR1NGU3lZa0RXaHdYZDh3RGNlS28vMQpMZkNVNkRrOElOL1NzcHBWVVdYUTJybE9SdnhsckhlQ2lvOG8wa1M5WWl1NTVXTVlnNGc9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=="
|
||||
}`,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
testLog := log.New(&logBuf, "", 0)
|
||||
exited := "exiting via fatal"
|
||||
fail = func(format string, v ...any) {
|
||||
testLog.Printf(format, v...)
|
||||
panic(exited)
|
||||
}
|
||||
|
||||
var sawSleep time.Duration
|
||||
sleep = func(d time.Duration) { sawSleep = d }
|
||||
|
||||
var sawOutput bytes.Buffer
|
||||
out = &sawOutput
|
||||
if tt.failOutput {
|
||||
out = &errWriter{}
|
||||
}
|
||||
|
||||
os.Args = tt.args
|
||||
getenv = func(key string) string { return tt.env[key] }
|
||||
if tt.wantFail {
|
||||
require.PanicsWithValue(t, exited, main)
|
||||
} else {
|
||||
require.NotPanics(t, main)
|
||||
}
|
||||
require.Equal(t, tt.wantSleep.String(), sawSleep.String())
|
||||
require.Equal(t, tt.wantLog, logBuf.String())
|
||||
if tt.wantOutJSON == "" {
|
||||
require.Empty(t, sawOutput.String())
|
||||
} else {
|
||||
require.JSONEq(t, tt.wantOutJSON, sawOutput.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
|
||||
cm5ldGVzMB4XDTIwMDcyNTIxMDQxOFoXDTMwMDcyMzIxMDQxOFowFTETMBEGA1UE
|
||||
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3K
|
||||
hYv2gIQ1Dwzh2cWMid+ofAnvLIfV2Xv61vTLGprUI+XUqB4/gtf6X6UNn0Lett2n
|
||||
d8p4wy7hw73hU/ggdvmWJvqBrSjc3JGfy+kj66fKXX+PTlbL7QbwiRvcSqIXIWlV
|
||||
lHHxECWrED8jCulw/NVqfook/h5iNUCT9yswSJr/0fImiVnoTlIoEYG2eCNejZ5c
|
||||
g39uD3ZTqd9ZxWwSLLnI+2kpJnZBPcd1ZQ8AQqzDgZtYRCqacn5gckQUKZWKQlxo
|
||||
Eft6g1XHJouAWAZw7hEtk0v8rG0/eKF7wamxFi6BFVlbjWBsB4T9rApbdBWTKeCJ
|
||||
Hv8fv5RMFSzpT3uzTO8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
|
||||
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACh5RhbxqJe+Z/gc17cZhKNmdiwu
|
||||
I2pLp3QBfwvN+Wbmajzw/7rYhY0d8JYVTJzXSCPWi6UAKxAtXOLF8WIIf9i39n6R
|
||||
uKOBGW14FzzGyRJiD3qaG/JTvEW+SLhwl68Ndr5LHSnbugAqq31abcQy6Zl9v5A8
|
||||
JKC97Lj/Sn8rj7opKy4W3oq7NCQsAb0zh4IllRF6UvSnJySfsg7xdXHHpxYDHtOS
|
||||
XcOu5ySUIZTgFe9RfeUZlGZ5xn0ckMlQ7qW2Wx1q0OVWw5us4NtkGqKrHG4Tn1X7
|
||||
uwo/Yytn5sDxrDv1/oii6AZOCsTPre4oD3wz4nmVzCVJcgrqH4Q24hT8WNg=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAvcqFi/aAhDUPDOHZxYyJ36h8Ce8sh9XZe/rW9MsamtQj5dSo
|
||||
Hj+C1/pfpQ2fQt623ad3ynjDLuHDveFT+CB2+ZYm+oGtKNzckZ/L6SPrp8pdf49O
|
||||
VsvtBvCJG9xKohchaVWUcfEQJasQPyMK6XD81Wp+iiT+HmI1QJP3KzBImv/R8iaJ
|
||||
WehOUigRgbZ4I16NnlyDf24PdlOp31nFbBIsucj7aSkmdkE9x3VlDwBCrMOBm1hE
|
||||
KppyfmByRBQplYpCXGgR+3qDVccmi4BYBnDuES2TS/ysbT94oXvBqbEWLoEVWVuN
|
||||
YGwHhP2sClt0FZMp4Ike/x+/lEwVLOlPe7NM7wIDAQABAoIBAFC1tUEmHNUcM0BJ
|
||||
M3D9KQzB+63F1mwVlx1QOOV1EeVR3co5Ox1R6PSr9sycFGQ9jgqI0zp5TJe9Tp6L
|
||||
GkhklfPh1MWnK9o6wlnzWKXWrrp2Jni+mpPyuOPAmq4Maniv2XeP+0bROwqpyojv
|
||||
AA7yC7M+TH226ZJGNVs3EV9+cwHml0yuzBfIJn/rv/w2g+WRKM/MC0S7k2d8bRlA
|
||||
NycKVGAGBhKTltjoVYOeh6aHEpSjK8zfaePjo5dYJvoVIli60YCgcJOU/8jXT+Np
|
||||
1Fm7tRvAtj3pUp0Sqdaf2RUzh9jfJp2VFCHuSJ6TPqArOyQojtMcTHF0TiW7xrHP
|
||||
xOCRIAECgYEAwGBPU7vdthMJBg+ORUoGQQaItTeJvQwIqJvbKD2osp4jhS1dGZBw
|
||||
W30GKEc/gd8JNtOq9BBnMicPF7hktuy+bSPv41XPud67rSSO7Tsw20C10gFRq06B
|
||||
zIJWFAUqK3IkvVc3VDmtSLSDox4QZ/BdqaMlQ5y5JCsC5kThmkZFlO8CgYEA/I9X
|
||||
YHi6RioMJE1fqOHJL4DDjlezmcuRrD7fE5InKbtJZ2JhGYOX/C0KXnHTOWTCDxxN
|
||||
FBvpvD6Xv5o3PhB9Z6k2fqvJ4GS8urkG/KU4xcC+bak+9ava8oaiSqG16zD9NH2P
|
||||
jJ60NrbLl1J0pU9fiwuFVUKJ4hDZOfN9RqYdyAECgYAVwo8WhJiGgM6zfcz073OX
|
||||
pVqPTPHqjVLpZ3+5pIfRdGvGI6R1QM5EuvaYVb7MPOM47WZX5wcVOC/P2g6iVlMP
|
||||
21HGIC2384a9BfaYxOo40q/+SiHnw6CQ9mkwKIllkqqvNA9RGpkMMUb2i28For2l
|
||||
c4vCgxa6DZdtXns6TRqPxwKBgCfY5cxOv/T6BVhk7MbUeM2J31DB/ZAyUhV/Bess
|
||||
kAlBh19MYk2IOZ6L7KriApV3lDaWHIMjtEkDByYvyq98Io0MYZCywfMpca10K+oI
|
||||
l2B7/I+IuGpCZxUEsO5dfTpSTGDPvqpND9niFVUWqVi7oTNq6ep9yQtl5SADjqxq
|
||||
4SABAoGAIm0hUg1wtcS46cGLy6PIkPM5tocTSghtz4vFsuk/i4QA9GBoBO2gH6ty
|
||||
+kJHmeaXt2dmgySp0QAWit5UlceEumB0NXnAdJZQxeGSFSyYkDWhwXd8wDceKo/1
|
||||
LfCU6Dk8IN/SsppVUWXQ2rlORvxlrHeCio8o0kS9Yiu55WMYg4g=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package main is the combined entrypoint for all Pinniped server components.
|
||||
//
|
||||
// It dispatches to the appropriate Main() entrypoint based the name it is invoked as (os.Args[0]). In our server
|
||||
// container image, this binary is symlinked to several names such as `/usr/local/bin/pinniped-concierge`.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
concierge "go.pinniped.dev/internal/concierge/server"
|
||||
// this side effect import ensures that we use fipsonly crypto in fips_strict mode.
|
||||
_ "go.pinniped.dev/internal/crypto/ptls"
|
||||
lua "go.pinniped.dev/internal/localuserauthenticator"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
supervisor "go.pinniped.dev/internal/supervisor/server"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals // these are swapped during unit tests.
|
||||
var (
|
||||
fail = plog.Fatal
|
||||
subcommands = map[string]func(){
|
||||
"pinniped-concierge": concierge.Main,
|
||||
"pinniped-supervisor": supervisor.Main,
|
||||
"local-user-authenticator": lua.Main,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 0 {
|
||||
fail(fmt.Errorf("missing os.Args"))
|
||||
}
|
||||
binary := filepath.Base(os.Args[0])
|
||||
if subcommands[binary] == nil {
|
||||
fail(fmt.Errorf("must be invoked as one of %v, not %q", sets.StringKeySet(subcommands).List(), binary))
|
||||
}
|
||||
subcommands[binary]()
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEntrypoint(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
args []string
|
||||
wantOutput string
|
||||
wantFail bool
|
||||
wantArgs []string
|
||||
}{
|
||||
{
|
||||
name: "missing args",
|
||||
args: []string{},
|
||||
wantOutput: "missing os.Args\n",
|
||||
wantFail: true,
|
||||
},
|
||||
{
|
||||
name: "invalid subcommand",
|
||||
args: []string{"/path/to/invalid", "some", "args"},
|
||||
wantOutput: "must be invoked as one of [another-test-binary valid-test-binary], not \"invalid\"\n",
|
||||
wantFail: true,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
args: []string{"/path/to/valid-test-binary", "foo", "bar"},
|
||||
wantArgs: []string{"/path/to/valid-test-binary", "foo", "bar"},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
testLog := log.New(&logBuf, "", 0)
|
||||
exited := "exiting via fatal"
|
||||
fail = func(err error, keysAndValues ...any) {
|
||||
testLog.Print(err)
|
||||
if len(keysAndValues) > 0 {
|
||||
testLog.Print(keysAndValues...)
|
||||
}
|
||||
panic(exited)
|
||||
}
|
||||
|
||||
// Make a test command that records os.Args when it's invoked.
|
||||
var gotArgs []string
|
||||
subcommands = map[string]func(){
|
||||
"valid-test-binary": func() { gotArgs = os.Args },
|
||||
"another-test-binary": func() {},
|
||||
}
|
||||
|
||||
os.Args = tt.args
|
||||
if tt.wantFail {
|
||||
require.PanicsWithValue(t, exited, main)
|
||||
} else {
|
||||
require.NotPanics(t, main)
|
||||
}
|
||||
if tt.wantArgs != nil {
|
||||
require.Equal(t, tt.wantArgs, gotArgs)
|
||||
}
|
||||
if tt.wantOutput != "" {
|
||||
require.Equal(t, tt.wantOutput, logBuf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.pinniped.dev/internal/httputil/roundtripper"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
type auditIDLoggerFunc func(path string, statusCode int, auditID string)
|
||||
|
||||
func logAuditID(path string, statusCode int, auditID string) {
|
||||
plog.Info("Received auditID for failed request",
|
||||
"path", path,
|
||||
"statusCode", statusCode,
|
||||
"auditID", auditID)
|
||||
}
|
||||
|
||||
func LogAuditIDTransportWrapper(rt http.RoundTripper) http.RoundTripper {
|
||||
return logAuditIDTransportWrapper(rt, logAuditID)
|
||||
}
|
||||
|
||||
func logAuditIDTransportWrapper(rt http.RoundTripper, auditIDLoggerFunc auditIDLoggerFunc) http.RoundTripper {
|
||||
return roundtripper.WrapFunc(rt, func(r *http.Request) (*http.Response, error) {
|
||||
response, responseErr := rt.RoundTrip(r)
|
||||
|
||||
if responseErr != nil ||
|
||||
response == nil ||
|
||||
response.Header.Get("audit-ID") == "" ||
|
||||
response.Request == nil ||
|
||||
response.Request.URL == nil {
|
||||
return response, responseErr
|
||||
}
|
||||
|
||||
// Use the request path from the response's request, in case the
|
||||
// original request was modified by any other roudtrippers in the chain.
|
||||
auditIDLoggerFunc(response.Request.URL.Path,
|
||||
response.StatusCode,
|
||||
response.Header.Get("audit-ID"))
|
||||
|
||||
return response, responseErr
|
||||
})
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/httputil/roundtripper"
|
||||
)
|
||||
|
||||
func TestLogAuditIDTransportWrapper(t *testing.T) {
|
||||
canonicalAuditIdHeaderName := "Audit-Id"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
response *http.Response
|
||||
responseErr error
|
||||
want func(t *testing.T, called func()) auditIDLoggerFunc
|
||||
wantCalled bool
|
||||
}{
|
||||
{
|
||||
name: "happy HTTP response - no error and no log",
|
||||
response: &http.Response{ // no headers
|
||||
StatusCode: http.StatusOK,
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Path: "some-path-from-response-request",
|
||||
},
|
||||
},
|
||||
},
|
||||
responseErr: nil,
|
||||
want: func(t *testing.T, called func()) auditIDLoggerFunc {
|
||||
return func(_ string, _ int, _ string) {
|
||||
called()
|
||||
}
|
||||
},
|
||||
wantCalled: false, // make it obvious
|
||||
},
|
||||
{
|
||||
name: "nil HTTP response - no error and no log",
|
||||
response: nil,
|
||||
responseErr: nil,
|
||||
want: func(t *testing.T, called func()) auditIDLoggerFunc {
|
||||
return func(_ string, _ int, _ string) {
|
||||
called()
|
||||
}
|
||||
},
|
||||
wantCalled: false, // make it obvious
|
||||
},
|
||||
{
|
||||
name: "err HTTP response - no error and no log",
|
||||
response: nil,
|
||||
responseErr: errors.New("some error"),
|
||||
want: func(t *testing.T, called func()) auditIDLoggerFunc {
|
||||
return func(_ string, _ int, _ string) {
|
||||
called()
|
||||
}
|
||||
},
|
||||
wantCalled: false, // make it obvious
|
||||
},
|
||||
{
|
||||
name: "happy HTTP response with audit-ID - logs",
|
||||
response: &http.Response{
|
||||
Header: http.Header{
|
||||
canonicalAuditIdHeaderName: []string{"some-audit-id", "some-other-audit-id-that-will-never-be-seen"},
|
||||
},
|
||||
StatusCode: http.StatusBadGateway, // statusCode does not matter
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Path: "some-path-from-response-request",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: func(t *testing.T, called func()) auditIDLoggerFunc {
|
||||
return func(path string, statusCode int, auditID string) {
|
||||
called()
|
||||
require.Equal(t, "some-path-from-response-request", path)
|
||||
require.Equal(t, http.StatusBadGateway, statusCode)
|
||||
require.Equal(t, "some-audit-id", auditID)
|
||||
}
|
||||
},
|
||||
wantCalled: true, // make it obvious
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
require.NotNil(t, test.want)
|
||||
|
||||
mockRequest := &http.Request{
|
||||
URL: &url.URL{
|
||||
Path: "should-never-use-this-path",
|
||||
},
|
||||
}
|
||||
var mockRt roundtripper.Func = func(r *http.Request) (*http.Response, error) {
|
||||
require.Equal(t, mockRequest, r)
|
||||
return test.response, test.responseErr
|
||||
}
|
||||
called := false
|
||||
subjectRt := logAuditIDTransportWrapper(mockRt, test.want(t, func() {
|
||||
called = true
|
||||
}))
|
||||
actualResponse, err := subjectRt.RoundTrip(mockRequest) //nolint:bodyclose // there is no Body.
|
||||
require.Equal(t, test.responseErr, err) // This roundtripper only returns mocked errors.
|
||||
require.Equal(t, test.response, actualResponse)
|
||||
require.Equal(t, test.wantCalled, called,
|
||||
"want logFunc to be called: %t, actually was called: %t", test.wantCalled, called)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// mustMarkRequired marks the given flags as required on the provided cobra.Command. If any of the names are wrong, it panics.
|
||||
func mustMarkRequired(cmd *cobra.Command, flags ...string) {
|
||||
for _, flag := range flags {
|
||||
if err := cmd.MarkFlagRequired(flag); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mustMarkHidden marks the given flags as hidden on the provided cobra.Command. If any of the names are wrong, it panics.
|
||||
func mustMarkHidden(cmd *cobra.Command, flags ...string) {
|
||||
for _, flag := range flags {
|
||||
if err := cmd.Flags().MarkHidden(flag); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarkDeprecated(cmd *cobra.Command, flag, usageMessage string) {
|
||||
if err := cmd.Flags().MarkDeprecated(flag, usageMessage); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMustMarkRequired(t *testing.T) {
|
||||
require.NotPanics(t, func() { mustMarkRequired(&cobra.Command{}) })
|
||||
require.NotPanics(t, func() {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("known-flag", "", "")
|
||||
mustMarkRequired(cmd, "known-flag")
|
||||
})
|
||||
require.Panics(t, func() { mustMarkRequired(&cobra.Command{}, "unknown-flag") })
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
conciergeconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
)
|
||||
|
||||
// conciergeModeFlag represents the method by which we should connect to the Concierge on a cluster during login.
|
||||
// this is meant to be a valid flag.Value implementation.
|
||||
type conciergeModeFlag int
|
||||
|
||||
var _ flag.Value = new(conciergeModeFlag)
|
||||
|
||||
const (
|
||||
modeUnknown conciergeModeFlag = iota
|
||||
modeTokenCredentialRequestAPI
|
||||
modeImpersonationProxy
|
||||
)
|
||||
|
||||
func (f *conciergeModeFlag) String() string {
|
||||
switch *f {
|
||||
case modeImpersonationProxy:
|
||||
return "ImpersonationProxy"
|
||||
case modeTokenCredentialRequestAPI:
|
||||
return "TokenCredentialRequestAPI"
|
||||
case modeUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return "TokenCredentialRequestAPI"
|
||||
}
|
||||
}
|
||||
|
||||
func (f *conciergeModeFlag) Set(s string) error {
|
||||
if strings.EqualFold(s, "") {
|
||||
*f = modeUnknown
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(s, "TokenCredentialRequestAPI") {
|
||||
*f = modeTokenCredentialRequestAPI
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(s, "ImpersonationProxy") {
|
||||
*f = modeImpersonationProxy
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid mode %q, valid modes are TokenCredentialRequestAPI and ImpersonationProxy", s)
|
||||
}
|
||||
|
||||
func (f *conciergeModeFlag) Type() string {
|
||||
return "mode"
|
||||
}
|
||||
|
||||
// MatchesFrontend returns true iff the flag matches the type of the provided frontend.
|
||||
func (f *conciergeModeFlag) MatchesFrontend(frontend *conciergeconfigv1alpha1.CredentialIssuerFrontend) bool {
|
||||
switch *f {
|
||||
case modeImpersonationProxy:
|
||||
return frontend.Type == conciergeconfigv1alpha1.ImpersonationProxyFrontendType
|
||||
case modeTokenCredentialRequestAPI:
|
||||
return frontend.Type == conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType
|
||||
case modeUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// caBundlePathsVar represents a list of CA bundle paths, which load from disk when the flag is populated.
|
||||
type caBundleFlag []byte
|
||||
|
||||
var _ pflag.Value = new(caBundleFlag)
|
||||
|
||||
func (f *caBundleFlag) String() string {
|
||||
return string(*f)
|
||||
}
|
||||
|
||||
func (f *caBundleFlag) Set(path string) error {
|
||||
pem, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read CA bundle path: %w", err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(pem) {
|
||||
return fmt.Errorf("failed to load any CA certificates from %q", path)
|
||||
}
|
||||
if len(*f) == 0 {
|
||||
*f = pem
|
||||
return nil
|
||||
}
|
||||
*f = bytes.Join([][]byte{*f, pem}, []byte("\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *caBundleFlag) Type() string {
|
||||
return "path"
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
conciergeconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
)
|
||||
|
||||
func TestConciergeModeFlag(t *testing.T) {
|
||||
var f conciergeModeFlag
|
||||
require.Equal(t, "mode", f.Type())
|
||||
require.Equal(t, modeUnknown, f)
|
||||
require.NoError(t, f.Set(""))
|
||||
require.Equal(t, modeUnknown, f)
|
||||
require.EqualError(t, f.Set("foo"), `invalid mode "foo", valid modes are TokenCredentialRequestAPI and ImpersonationProxy`)
|
||||
require.True(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.True(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.ImpersonationProxyFrontendType}))
|
||||
|
||||
require.NoError(t, f.Set("TokenCredentialRequestAPI"))
|
||||
require.Equal(t, modeTokenCredentialRequestAPI, f)
|
||||
require.Equal(t, "TokenCredentialRequestAPI", f.String())
|
||||
require.True(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.False(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.ImpersonationProxyFrontendType}))
|
||||
|
||||
require.NoError(t, f.Set("tokencredentialrequestapi"))
|
||||
require.Equal(t, modeTokenCredentialRequestAPI, f)
|
||||
require.Equal(t, "TokenCredentialRequestAPI", f.String())
|
||||
|
||||
require.NoError(t, f.Set("ImpersonationProxy"))
|
||||
require.Equal(t, modeImpersonationProxy, f)
|
||||
require.Equal(t, "ImpersonationProxy", f.String())
|
||||
require.False(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.True(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.ImpersonationProxyFrontendType}))
|
||||
|
||||
require.NoError(t, f.Set("impersonationproxy"))
|
||||
require.Equal(t, modeImpersonationProxy, f)
|
||||
require.Equal(t, "ImpersonationProxy", f.String())
|
||||
}
|
||||
|
||||
func TestCABundleFlag(t *testing.T) {
|
||||
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
tmpdir := t.TempDir()
|
||||
emptyFilePath := filepath.Join(tmpdir, "empty")
|
||||
require.NoError(t, os.WriteFile(emptyFilePath, []byte{}, 0600))
|
||||
|
||||
testCAPath := filepath.Join(tmpdir, "testca.pem")
|
||||
require.NoError(t, os.WriteFile(testCAPath, testCA.Bundle(), 0600))
|
||||
|
||||
f := caBundleFlag{}
|
||||
require.Equal(t, "path", f.Type())
|
||||
require.Equal(t, "", f.String())
|
||||
require.EqualError(t, f.Set("./does/not/exist"), "could not read CA bundle path: open ./does/not/exist: no such file or directory")
|
||||
require.EqualError(t, f.Set(emptyFilePath), fmt.Sprintf("failed to load any CA certificates from %q", emptyFilePath))
|
||||
|
||||
require.NoError(t, f.Set(testCAPath))
|
||||
require.Equal(t, 1, bytes.Count(f, []byte("BEGIN CERTIFICATE")))
|
||||
|
||||
require.NoError(t, f.Set(testCAPath))
|
||||
require.Equal(t, 2, bytes.Count(f, []byte("BEGIN CERTIFICATE")))
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(generateMarkdownHelpCommand())
|
||||
}
|
||||
|
||||
func generateMarkdownHelpCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "generate-markdown-help",
|
||||
Short: "Generate markdown help for the current set of non-hidden CLI commands",
|
||||
SilenceUsage: true, // do not print usage message when commands fail
|
||||
Hidden: true,
|
||||
RunE: runGenerateMarkdownHelp,
|
||||
}
|
||||
}
|
||||
|
||||
func runGenerateMarkdownHelp(cmd *cobra.Command, _ []string) error {
|
||||
var generated bytes.Buffer
|
||||
if err := generate(&generated); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := write(cmd.OutOrStdout(), &generated, "###### Auto generated by spf13/cobra"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generate(w io.Writer) error {
|
||||
if err := generateHeader(w); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := generateCommand(w, rootCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateHeader(w io.Writer) error {
|
||||
_, err := fmt.Fprintf(w, `---
|
||||
title: Command-Line Options Reference
|
||||
description: Reference for the `+"`pinniped`"+` command-line tool
|
||||
cascade:
|
||||
layout: docs
|
||||
menu:
|
||||
docs:
|
||||
name: Command-Line Options
|
||||
weight: 30
|
||||
parent: reference
|
||||
---
|
||||
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func generateCommand(w io.Writer, command *cobra.Command) error {
|
||||
for _, command := range command.Commands() {
|
||||
// if this node is hidden, don't traverse it or its descendents
|
||||
if command.Hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
// generate children
|
||||
if err := generateCommand(w, command); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// generate self, but only if we are a command that people would run to do something interesting
|
||||
if command.Run != nil || command.RunE != nil {
|
||||
if err := doc.GenMarkdownCustom(command, w, func(_ string) string { return "" }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func write(w io.Writer, r io.Reader, unwantedPrefixes ...string) error {
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
if !containsPrefix(line, unwantedPrefixes) {
|
||||
if _, err := fmt.Fprintln(w, line); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.Err()
|
||||
}
|
||||
|
||||
func containsPrefix(s string, prefixes []string) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(s, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Gets one of [kubeconfig]",
|
||||
SilenceUsage: true, // Do not print usage message when commands fail.
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(getCmd)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
|
||||
|
||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
)
|
||||
|
||||
// getClientsetsFunc is a function that can return clients for the Concierge and Kubernetes APIs given a
|
||||
// clientConfig and the apiGroupSuffix with which the API is running.
|
||||
type getClientsetsFunc func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, kubernetes.Interface, aggregatorclient.Interface, error)
|
||||
|
||||
// getRealClientsets returns real implementations of the Concierge and Kubernetes client interfaces.
|
||||
func getRealClientsets(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, kubernetes.Interface, aggregatorclient.Interface, error) {
|
||||
restConfig, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
client, err := kubeclient.New(
|
||||
kubeclient.WithConfig(restConfig),
|
||||
kubeclient.WithMiddleware(groupsuffix.New(apiGroupSuffix)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return client.PinnipedConcierge, client.Kubernetes, client.Aggregation, nil
|
||||
}
|
||||
|
||||
// newClientConfig returns a clientcmd.ClientConfig given an optional kubeconfig path override and
|
||||
// an optional context override.
|
||||
func newClientConfig(kubeconfigPathOverride string, currentContextName string) clientcmd.ClientConfig {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.ExplicitPath = kubeconfigPathOverride
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{
|
||||
CurrentContext: currentContextName,
|
||||
})
|
||||
return clientConfig
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
"k8s.io/client-go/tools/auth/exec"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Authenticates with one of [oidc, static]",
|
||||
Long: here.Doc(
|
||||
`Authenticates with one of [oidc, static]
|
||||
|
||||
Use "pinniped get kubeconfig" to generate a kubeconfig file which will include
|
||||
one of these login subcommands in its configuration. The oidc and static
|
||||
subcommands are not meant to be invoked directly by a user.
|
||||
|
||||
The oidc and static subcommands are Kubernetes client-go credential plugins
|
||||
which are meant to be configured inside a kubeconfig file. (See the Kubernetes
|
||||
authentication documentation for more information about client-go credential
|
||||
plugins.)`,
|
||||
),
|
||||
SilenceUsage: true, // Do not print usage message when commands fail.
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
}
|
||||
|
||||
func loadClusterInfo() *clientauthv1beta1.Cluster {
|
||||
obj, _, err := exec.LoadExecCredentialFromEnv()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cred, ok := obj.(*clientauthv1beta1.ExecCredential)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return cred.Spec.Cluster
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||
"go.pinniped.dev/internal/execcredcache"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/net/phttp"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
|
||||
const (
|
||||
// The user may override the flow selection made by `--upstream-identity-provider-flow` using an env var.
|
||||
// This allows the user to override their default flow selected inside their Pinniped-compatible kubeconfig file.
|
||||
// A user might want to use this env var, for example, to choose the "browser_authcode" flow when using a kubeconfig
|
||||
// which specifies "cli_password" when using an IDE plugin where there is no interactive CLI available. This allows
|
||||
// the user to use one kubeconfig file for both flows.
|
||||
upstreamIdentityProviderFlowEnvVarName = "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW"
|
||||
|
||||
// When using a browser-based login flow, the user may skip printing the login URL to the screen in the case
|
||||
// where the browser was launched with the login URL. This can be useful, for example, when using a console-based
|
||||
// UI like k9s, to avoid having any output to stderr which may confuse the UI. Set this env var to "true" to
|
||||
// skip printing the URL.
|
||||
skipPrintLoginURLEnvVarName = "PINNIPED_SKIP_PRINT_LOGIN_URL"
|
||||
|
||||
// Set this env var to "true" to cause debug logs to be printed to stderr.
|
||||
debugEnvVarName = "PINNIPED_DEBUG"
|
||||
|
||||
// The value to use for true/false env vars to enable the behavior caused by the env var.
|
||||
envVarTruthyValue = "true"
|
||||
)
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps()))
|
||||
}
|
||||
|
||||
type oidcLoginCommandDeps struct {
|
||||
lookupEnv func(string) (string, bool)
|
||||
login func(string, string, ...oidcclient.Option) (*oidctypes.Token, error)
|
||||
exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error)
|
||||
optionsFactory OIDCClientOptions
|
||||
}
|
||||
|
||||
func oidcLoginCommandRealDeps() oidcLoginCommandDeps {
|
||||
return oidcLoginCommandDeps{
|
||||
lookupEnv: os.LookupEnv,
|
||||
login: oidcclient.Login,
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
return client.ExchangeToken(ctx, token)
|
||||
},
|
||||
optionsFactory: &clientOptions{},
|
||||
}
|
||||
}
|
||||
|
||||
type oidcLoginFlags struct {
|
||||
issuer string
|
||||
clientID string
|
||||
listenPort uint16
|
||||
scopes []string
|
||||
skipBrowser bool
|
||||
skipListen bool
|
||||
sessionCachePath string
|
||||
caBundlePaths []string
|
||||
caBundleData []string
|
||||
debugSessionCache bool
|
||||
requestAudience string
|
||||
conciergeEnabled bool
|
||||
conciergeAuthenticatorType string
|
||||
conciergeAuthenticatorName string
|
||||
conciergeEndpoint string
|
||||
conciergeCABundle string
|
||||
conciergeAPIGroupSuffix string
|
||||
credentialCachePath string
|
||||
upstreamIdentityProviderName string
|
||||
upstreamIdentityProviderType string
|
||||
upstreamIdentityProviderFlow string
|
||||
}
|
||||
|
||||
func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "oidc --issuer ISSUER",
|
||||
Short: "Login using an OpenID Connect provider",
|
||||
Long: here.Doc(
|
||||
`Login using an OpenID Connect provider
|
||||
|
||||
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
|
||||
login command in its configuration. This login command is not meant to be
|
||||
invoked directly by a user.
|
||||
|
||||
This login command is a Kubernetes client-go credential plugin which is meant to
|
||||
be configured inside a kubeconfig file. (See the Kubernetes authentication
|
||||
documentation for more information about client-go credential plugins.)`,
|
||||
),
|
||||
SilenceUsage: true, // do not print usage message when commands fail
|
||||
}
|
||||
flags oidcLoginFlags
|
||||
conciergeNamespace string // unused now
|
||||
)
|
||||
cmd.Flags().StringVar(&flags.issuer, "issuer", "", "OpenID Connect issuer URL")
|
||||
cmd.Flags().StringVar(&flags.clientID, "client-id", oidcapi.ClientIDPinnipedCLI, "OpenID Connect client ID")
|
||||
cmd.Flags().Uint16Var(&flags.listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
||||
cmd.Flags().StringSliceVar(&flags.scopes, "scopes", []string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups}, "OIDC scopes to request during login")
|
||||
cmd.Flags().BoolVar(&flags.skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL)")
|
||||
cmd.Flags().BoolVar(&flags.skipListen, "skip-listen", false, "Skip starting a localhost callback listener (manual copy/paste flow only)")
|
||||
cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file")
|
||||
cmd.Flags().StringSliceVar(&flags.caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
||||
cmd.Flags().StringSliceVar(&flags.caBundleData, "ca-bundle-data", nil, "Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)")
|
||||
cmd.Flags().BoolVar(&flags.debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache")
|
||||
cmd.Flags().StringVar(&flags.requestAudience, "request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
|
||||
cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Use the Concierge to login")
|
||||
cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed")
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')")
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
|
||||
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge")
|
||||
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)")
|
||||
cmd.Flags().StringVar(&flags.upstreamIdentityProviderName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor")
|
||||
cmd.Flags().StringVar(&flags.upstreamIdentityProviderType,
|
||||
"upstream-identity-provider-type",
|
||||
idpdiscoveryv1alpha1.IDPTypeOIDC.String(),
|
||||
fmt.Sprintf(
|
||||
"The type of the upstream identity provider used during login with a Supervisor (e.g. '%s', '%s', '%s', '%s')",
|
||||
idpdiscoveryv1alpha1.IDPTypeOIDC,
|
||||
idpdiscoveryv1alpha1.IDPTypeLDAP,
|
||||
idpdiscoveryv1alpha1.IDPTypeActiveDirectory,
|
||||
idpdiscoveryv1alpha1.IDPTypeGitHub,
|
||||
))
|
||||
cmd.Flags().StringVar(&flags.upstreamIdentityProviderFlow, "upstream-identity-provider-flow", "", fmt.Sprintf("The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. '%s', '%s')", idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode, idpdiscoveryv1alpha1.IDPFlowCLIPassword))
|
||||
|
||||
// --skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case.
|
||||
mustMarkHidden(cmd, "skip-listen")
|
||||
mustMarkHidden(cmd, "debug-session-cache")
|
||||
mustMarkRequired(cmd, "issuer")
|
||||
cmd.RunE = func(cmd *cobra.Command, _args []string) error { return runOIDCLogin(cmd, deps, flags) }
|
||||
|
||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||
mustMarkHidden(cmd, "concierge-namespace")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { //nolint:funlen
|
||||
pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv)
|
||||
if err != nil {
|
||||
plog.WarningErr("Received error while setting log level", err)
|
||||
}
|
||||
|
||||
// Initialize the session cache.
|
||||
var sessionOptions []filesession.Option
|
||||
|
||||
// If the hidden --debug-session-cache option is passed, log all the errors from the session cache.
|
||||
if flags.debugSessionCache {
|
||||
sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) {
|
||||
pLogger.Error("error during session cache operation", err)
|
||||
}))
|
||||
}
|
||||
sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...)
|
||||
|
||||
// Initialize the login handler.
|
||||
opts := []oidcclient.Option{
|
||||
deps.optionsFactory.WithContext(cmd.Context()),
|
||||
deps.optionsFactory.WithLoginLogger(pLogger),
|
||||
deps.optionsFactory.WithScopes(flags.scopes),
|
||||
deps.optionsFactory.WithSessionCache(sessionCache),
|
||||
}
|
||||
|
||||
skipPrintLoginURL, _ := deps.lookupEnv(skipPrintLoginURLEnvVarName)
|
||||
if skipPrintLoginURL == envVarTruthyValue {
|
||||
opts = append(opts, deps.optionsFactory.WithSkipPrintLoginURL())
|
||||
}
|
||||
|
||||
if flags.listenPort != 0 {
|
||||
opts = append(opts, deps.optionsFactory.WithListenPort(flags.listenPort))
|
||||
}
|
||||
|
||||
if flags.requestAudience != "" {
|
||||
opts = append(opts, deps.optionsFactory.WithRequestAudience(flags.requestAudience))
|
||||
}
|
||||
|
||||
if flags.upstreamIdentityProviderName != "" {
|
||||
opts = append(opts, deps.optionsFactory.WithUpstreamIdentityProvider(
|
||||
flags.upstreamIdentityProviderName, flags.upstreamIdentityProviderType))
|
||||
}
|
||||
|
||||
requestedFlow, flowSource := idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow), "--upstream-identity-provider-flow"
|
||||
if flowOverride, hasFlowOverride := deps.lookupEnv(upstreamIdentityProviderFlowEnvVarName); hasFlowOverride {
|
||||
requestedFlow, flowSource = idpdiscoveryv1alpha1.IDPFlow(flowOverride), upstreamIdentityProviderFlowEnvVarName
|
||||
}
|
||||
if requestedFlow != "" {
|
||||
opts = append(opts, deps.optionsFactory.WithLoginFlow(requestedFlow, flowSource))
|
||||
}
|
||||
|
||||
var concierge *conciergeclient.Client
|
||||
if flags.conciergeEnabled {
|
||||
var err error
|
||||
concierge, err = conciergeclient.New(
|
||||
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
|
||||
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
|
||||
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
|
||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||
conciergeclient.WithTransportWrapper(LogAuditIDTransportWrapper),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Concierge parameters: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --skip-browser skips opening the browser.
|
||||
if flags.skipBrowser {
|
||||
opts = append(opts, deps.optionsFactory.WithSkipBrowserOpen())
|
||||
}
|
||||
|
||||
// --skip-listen skips starting the localhost callback listener.
|
||||
if flags.skipListen {
|
||||
opts = append(opts, deps.optionsFactory.WithSkipListen())
|
||||
}
|
||||
|
||||
if len(flags.caBundlePaths) > 0 || len(flags.caBundleData) > 0 {
|
||||
client, err := makeClient(flags.caBundlePaths, flags.caBundleData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts = append(opts, deps.optionsFactory.WithClient(client))
|
||||
}
|
||||
// Look up cached credentials based on a hash of all the CLI arguments and the cluster info.
|
||||
cacheKey := struct {
|
||||
Args []string `json:"args"`
|
||||
ClusterInfo *clientauthv1beta1.Cluster `json:"cluster"`
|
||||
}{
|
||||
Args: os.Args[1:],
|
||||
ClusterInfo: loadClusterInfo(),
|
||||
}
|
||||
var credCache *execcredcache.Cache
|
||||
if flags.credentialCachePath != "" {
|
||||
credCache = execcredcache.New(flags.credentialCachePath)
|
||||
if cred := credCache.Get(cacheKey); cred != nil {
|
||||
pLogger.Debug("using cached cluster credential.")
|
||||
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
|
||||
}
|
||||
}
|
||||
|
||||
pLogger.Debug("Performing OIDC login", "issuer", flags.issuer, "client id", flags.clientID)
|
||||
// Do the basic login to get an OIDC token. Although this can return several tokens, we only need the ID token here.
|
||||
token, err := deps.login(flags.issuer, flags.clientID, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not complete Pinniped login: %w", err)
|
||||
}
|
||||
cred := tokenCredential(token.IDToken)
|
||||
|
||||
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
|
||||
if concierge != nil {
|
||||
pLogger.Debug("Exchanging token for cluster credential", "endpoint", flags.conciergeEndpoint, "authenticator type", flags.conciergeAuthenticatorType, "authenticator name", flags.conciergeAuthenticatorName)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cred, err = deps.exchangeToken(ctx, concierge, token.IDToken.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
|
||||
}
|
||||
pLogger.Debug("Successfully exchanged token for cluster credential.")
|
||||
} else {
|
||||
pLogger.Debug("No concierge configured, skipping token credential exchange")
|
||||
}
|
||||
|
||||
// If there was a credential cache, save the resulting credential for future use.
|
||||
if credCache != nil {
|
||||
pLogger.Debug("caching cluster credential for future use.")
|
||||
credCache.Put(cacheKey, cred)
|
||||
}
|
||||
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
|
||||
}
|
||||
|
||||
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
|
||||
pool := x509.NewCertPool()
|
||||
for _, p := range caBundlePaths {
|
||||
pem, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read --ca-bundle: %w", err)
|
||||
}
|
||||
pool.AppendCertsFromPEM(pem)
|
||||
}
|
||||
for _, d := range caBundleData {
|
||||
pem, err := base64.StdEncoding.DecodeString(d)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read --ca-bundle-data: %w", err)
|
||||
}
|
||||
pool.AppendCertsFromPEM(pem)
|
||||
}
|
||||
return phttp.Default(pool), nil
|
||||
}
|
||||
|
||||
func tokenCredential(idToken *oidctypes.IDToken) *clientauthv1beta1.ExecCredential {
|
||||
cred := clientauthv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ExecCredential",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: &clientauthv1beta1.ExecCredentialStatus{
|
||||
Token: idToken.Token,
|
||||
},
|
||||
}
|
||||
if !idToken.Expiry.IsZero() {
|
||||
cred.Status.ExpirationTimestamp = &idToken.Expiry
|
||||
}
|
||||
return &cred
|
||||
}
|
||||
|
||||
func SetLogLevel(ctx context.Context, lookupEnv func(string) (string, bool)) (plog.Logger, error) {
|
||||
debug, _ := lookupEnv(debugEnvVarName)
|
||||
if debug == envVarTruthyValue {
|
||||
err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug, Format: plog.FormatCLI})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return plog.New(), nil
|
||||
}
|
||||
|
||||
/*
|
||||
mustGetConfigDir returns a directory that follows the XDG base directory convention:
|
||||
|
||||
$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should
|
||||
be stored. If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used.
|
||||
|
||||
[1] https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
*/
|
||||
func mustGetConfigDir() string {
|
||||
const xdgAppName = "pinniped"
|
||||
|
||||
if path := os.Getenv("XDG_CONFIG_HOME"); path != "" {
|
||||
return filepath.Join(path, xdgAppName)
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return filepath.Join(home, ".config", xdgAppName)
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
clocktesting "k8s.io/utils/clock/testing"
|
||||
|
||||
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/mocks/mockoidcclientoptions"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
|
||||
func TestLoginOIDCCommand(t *testing.T) {
|
||||
cfgDir := mustGetConfigDir()
|
||||
|
||||
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
tmpdir := t.TempDir()
|
||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||
require.NoError(t, os.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
|
||||
|
||||
time1 := time.Date(3020, 10, 12, 13, 14, 15, 16, time.UTC)
|
||||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2028-10-11T23:37:26.953313745Z")
|
||||
require.NoError(t, err)
|
||||
nowStr := now.Local().Format(time.RFC1123)
|
||||
|
||||
defaultWantedOptions := func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
f.EXPECT().WithContext(gomock.Any())
|
||||
f.EXPECT().WithLoginLogger(gomock.Any())
|
||||
f.EXPECT().WithScopes([]string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups})
|
||||
f.EXPECT().WithSessionCache(gomock.Any())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
loginErr error
|
||||
conciergeErr error
|
||||
env map[string]string
|
||||
wantError bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantOptions func(f *mockoidcclientoptions.MockOIDCClientOptions)
|
||||
wantOptionsCount int
|
||||
wantLogs []string
|
||||
}{
|
||||
{
|
||||
name: "help flag passed",
|
||||
args: []string{"--help"},
|
||||
wantStdout: here.Doc(`
|
||||
Login using an OpenID Connect provider
|
||||
|
||||
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
|
||||
login command in its configuration. This login command is not meant to be
|
||||
invoked directly by a user.
|
||||
|
||||
This login command is a Kubernetes client-go credential plugin which is meant to
|
||||
be configured inside a kubeconfig file. (See the Kubernetes authentication
|
||||
documentation for more information about client-go credential plugins.)
|
||||
|
||||
Usage:
|
||||
oidc --issuer ISSUER [flags]
|
||||
|
||||
Flags:
|
||||
--ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
||||
--ca-bundle-data strings Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)
|
||||
--client-id string OpenID Connect client ID (default "pinniped-cli")
|
||||
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||
--concierge-authenticator-name string Concierge authenticator name
|
||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge
|
||||
--concierge-endpoint string API base for the Concierge endpoint
|
||||
--credential-cache string Path to cluster-specific credentials cache ("" disables the cache) (default "` + cfgDir + `/credentials.yaml")
|
||||
--enable-concierge Use the Concierge to login
|
||||
-h, --help help for oidc
|
||||
--issuer string OpenID Connect issuer URL
|
||||
--listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
||||
--request-audience string Request a token with an alternate audience using RFC8693 token exchange
|
||||
--scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience,username,groups])
|
||||
--session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml")
|
||||
--skip-browser Skip opening the browser (just print the URL)
|
||||
--upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'browser_authcode', 'cli_password')
|
||||
--upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor
|
||||
--upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') (default "oidc")
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing required flags",
|
||||
args: []string{},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: required flag(s) "issuer" not set
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing concierge flags",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--enable-concierge",
|
||||
},
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: endpoint must not be empty
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid CA bundle path",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--ca-bundle", "./does/not/exist",
|
||||
},
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not read --ca-bundle: open ./does/not/exist: no such file or directory
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid CA bundle data",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--ca-bundle-data", "invalid-base64",
|
||||
},
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not read --ca-bundle-data: illegal base64 data at input byte 7
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid API group suffix",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--enable-concierge",
|
||||
"--concierge-api-group-suffix", ".starts.with.dot",
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
},
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with default flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "PINNIPED_SKIP_PRINT_LOGIN_URL adds an option",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_SKIP_PRINT_LOGIN_URL": "true"},
|
||||
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
defaultWantedOptions(f)
|
||||
f.EXPECT().WithSkipPrintLoginURL()
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "--upstream-identity-provider-flow adds an option",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
defaultWantedOptions(f)
|
||||
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlowCLIPassword, "--upstream-identity-provider-flow")
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW adds an option that overrides --upstream-identity-provider-flow",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-flow", "ignored-value-from-param",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "actual-value-from-env"},
|
||||
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
defaultWantedOptions(f)
|
||||
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlow("actual-value-from-env"), "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW")
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "login error",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
loginErr: fmt.Errorf("some login error"),
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantOptionsCount: 4,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not complete Pinniped login: some login error
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "concierge token exchange error",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--enable-concierge",
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
conciergeErr: fmt.Errorf("some concierge error"),
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantOptionsCount: 4,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not complete Concierge credential exchange: some concierge error
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "success with minimal options",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true"},
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
wantLogs: []string{
|
||||
nowStr + ` cmd/login_oidc.go:268 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
|
||||
nowStr + ` cmd/login_oidc.go:288 No concierge configured, skipping token credential exchange`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success with all options",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--skip-browser",
|
||||
"--skip-listen",
|
||||
"--listen-port", "1234",
|
||||
"--debug-session-cache",
|
||||
"--request-audience", "cluster-1234",
|
||||
"--ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()),
|
||||
"--ca-bundle", testCABundlePath,
|
||||
"--enable-concierge",
|
||||
"--concierge-authenticator-type", "webhook",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
"--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()),
|
||||
"--concierge-api-group-suffix", "some.suffix.com",
|
||||
"--credential-cache", t.TempDir() + "/credentials.yaml", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
"--upstream-identity-provider-name", "some-upstream-name",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "some-flow-type",
|
||||
},
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true", "PINNIPED_SKIP_PRINT_LOGIN_URL": "true"},
|
||||
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
f.EXPECT().WithContext(gomock.Any())
|
||||
f.EXPECT().WithLoginLogger(gomock.Any())
|
||||
f.EXPECT().WithScopes([]string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups})
|
||||
f.EXPECT().WithSessionCache(gomock.Any())
|
||||
f.EXPECT().WithListenPort(uint16(1234))
|
||||
f.EXPECT().WithSkipBrowserOpen()
|
||||
f.EXPECT().WithSkipListen()
|
||||
f.EXPECT().WithSkipPrintLoginURL()
|
||||
f.EXPECT().WithClient(gomock.Any())
|
||||
f.EXPECT().WithRequestAudience("cluster-1234")
|
||||
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlow("some-flow-type"), "--upstream-identity-provider-flow")
|
||||
f.EXPECT().WithUpstreamIdentityProvider("some-upstream-name", "ldap")
|
||||
},
|
||||
wantOptionsCount: 12,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n",
|
||||
wantLogs: []string{
|
||||
nowStr + ` cmd/login_oidc.go:268 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
|
||||
nowStr + ` cmd/login_oidc.go:278 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
|
||||
nowStr + ` cmd/login_oidc.go:286 Successfully exchanged token for cluster credential.`,
|
||||
nowStr + ` cmd/login_oidc.go:293 caching cluster credential for future use.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
ctx := plog.AddZapOverridesToContext(context.Background(), t, &buf, nil, clocktesting.NewFakeClock(now))
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
t.Cleanup(ctrl.Finish)
|
||||
optionsFactory := mockoidcclientoptions.NewMockOIDCClientOptions(ctrl)
|
||||
if tt.wantOptions != nil {
|
||||
tt.wantOptions(optionsFactory)
|
||||
}
|
||||
|
||||
var gotOptions []oidcclient.Option
|
||||
cmd := oidcLoginCommand(oidcLoginCommandDeps{
|
||||
lookupEnv: func(s string) (string, bool) {
|
||||
v, ok := tt.env[s]
|
||||
return v, ok
|
||||
},
|
||||
login: func(issuer string, clientID string, opts ...oidcclient.Option) (*oidctypes.Token, error) {
|
||||
require.Equal(t, "test-issuer", issuer)
|
||||
require.Equal(t, "test-client-id", clientID)
|
||||
gotOptions = opts
|
||||
if tt.loginErr != nil {
|
||||
return nil, tt.loginErr
|
||||
}
|
||||
return &oidctypes.Token{
|
||||
IDToken: &oidctypes.IDToken{
|
||||
Token: "test-id-token",
|
||||
Expiry: metav1.NewTime(time1),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
require.Equal(t, token, "test-id-token")
|
||||
if tt.conciergeErr != nil {
|
||||
return nil, tt.conciergeErr
|
||||
}
|
||||
return &clientauthv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ExecCredential",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: &clientauthv1beta1.ExecCredentialStatus{
|
||||
Token: "exchanged-token",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
optionsFactory: optionsFactory,
|
||||
})
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs(tt.args)
|
||||
err = cmd.ExecuteContext(ctx)
|
||||
if tt.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
||||
require.Len(t, gotOptions, tt.wantOptionsCount)
|
||||
|
||||
require.Equal(t, tt.wantLogs, testutil.SplitByNewline(buf.String()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
"go.pinniped.dev/internal/execcredcache"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps()))
|
||||
}
|
||||
|
||||
type staticLoginDeps struct {
|
||||
lookupEnv func(string) (string, bool)
|
||||
exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error)
|
||||
}
|
||||
|
||||
func staticLoginRealDeps() staticLoginDeps {
|
||||
return staticLoginDeps{
|
||||
lookupEnv: os.LookupEnv,
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
return client.ExchangeToken(ctx, token)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type staticLoginParams struct {
|
||||
staticToken string
|
||||
staticTokenEnvName string
|
||||
conciergeEnabled bool
|
||||
conciergeAuthenticatorType string
|
||||
conciergeAuthenticatorName string
|
||||
conciergeEndpoint string
|
||||
conciergeCABundle string
|
||||
conciergeAPIGroupSuffix string
|
||||
credentialCachePath string
|
||||
}
|
||||
|
||||
func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "static [--token TOKEN] [--token-env TOKEN_NAME]",
|
||||
Short: "Login using a static token",
|
||||
Long: here.Doc(
|
||||
`Login using a static token
|
||||
|
||||
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
|
||||
login command in its configuration. This login command is not meant to be
|
||||
invoked directly by a user.
|
||||
|
||||
This login command is a Kubernetes client-go credential plugin which is meant to
|
||||
be configured inside a kubeconfig file. (See the Kubernetes authentication
|
||||
documentation for more information about client-go credential plugins.)`,
|
||||
),
|
||||
SilenceUsage: true, // do not print usage message when commands fail
|
||||
}
|
||||
flags staticLoginParams
|
||||
conciergeNamespace string // unused now
|
||||
)
|
||||
cmd.Flags().StringVar(&flags.staticToken, "token", "", "Static token to present during login")
|
||||
cmd.Flags().StringVar(&flags.staticTokenEnvName, "token-env", "", "Environment variable containing a static token")
|
||||
cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Use the Concierge to login")
|
||||
cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed")
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')")
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
|
||||
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge")
|
||||
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)")
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, _args []string) error { return runStaticLogin(cmd, deps, flags) }
|
||||
|
||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||
mustMarkHidden(cmd, "concierge-namespace")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginParams) error {
|
||||
out := cmd.OutOrStdout()
|
||||
pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv)
|
||||
if err != nil {
|
||||
plog.WarningErr("Received error while setting log level", err)
|
||||
}
|
||||
|
||||
if flags.staticToken == "" && flags.staticTokenEnvName == "" {
|
||||
return fmt.Errorf("one of --token or --token-env must be set")
|
||||
}
|
||||
|
||||
var concierge *conciergeclient.Client
|
||||
if flags.conciergeEnabled {
|
||||
var err error
|
||||
concierge, err = conciergeclient.New(
|
||||
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
|
||||
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
|
||||
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
|
||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||
conciergeclient.WithTransportWrapper(LogAuditIDTransportWrapper),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Concierge parameters: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var token string
|
||||
if flags.staticToken != "" {
|
||||
token = flags.staticToken
|
||||
}
|
||||
if flags.staticTokenEnvName != "" {
|
||||
var ok bool
|
||||
token, ok = deps.lookupEnv(flags.staticTokenEnvName)
|
||||
if !ok {
|
||||
return fmt.Errorf("--token-env variable %q is not set", flags.staticTokenEnvName)
|
||||
}
|
||||
if token == "" {
|
||||
return fmt.Errorf("--token-env variable %q is empty", flags.staticTokenEnvName)
|
||||
}
|
||||
}
|
||||
cred := tokenCredential(&oidctypes.IDToken{Token: token})
|
||||
|
||||
// Look up cached credentials based on a hash of all the CLI arguments, the current token value, and the cluster info.
|
||||
cacheKey := struct {
|
||||
Args []string `json:"args"`
|
||||
Token string `json:"token"`
|
||||
ClusterInfo *clientauthv1beta1.Cluster `json:"cluster"`
|
||||
}{
|
||||
Args: os.Args[1:],
|
||||
Token: token,
|
||||
ClusterInfo: loadClusterInfo(),
|
||||
}
|
||||
var credCache *execcredcache.Cache
|
||||
if flags.credentialCachePath != "" {
|
||||
credCache = execcredcache.New(flags.credentialCachePath)
|
||||
if cred := credCache.Get(cacheKey); cred != nil {
|
||||
pLogger.Debug("using cached cluster credential.")
|
||||
return json.NewEncoder(out).Encode(cred)
|
||||
}
|
||||
}
|
||||
|
||||
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
|
||||
if concierge != nil {
|
||||
pLogger.Debug("exchanging static token for cluster credential", "endpoint", flags.conciergeEndpoint, "authenticator type", flags.conciergeAuthenticatorType, "authenticator name", flags.conciergeAuthenticatorName)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
cred, err = deps.exchangeToken(ctx, concierge, token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
|
||||
}
|
||||
pLogger.Debug("exchanged static token for cluster credential")
|
||||
}
|
||||
|
||||
// If there was a credential cache, save the resulting credential for future use. We only save to the cache if
|
||||
// the credential came from the concierge, since that's the only static token case where the cache is useful.
|
||||
if credCache != nil && concierge != nil {
|
||||
credCache.Put(cacheKey, cred)
|
||||
}
|
||||
|
||||
return json.NewEncoder(out).Encode(cred)
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
clocktesting "k8s.io/utils/clock/testing"
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
)
|
||||
|
||||
func TestLoginStaticCommand(t *testing.T) {
|
||||
cfgDir := mustGetConfigDir()
|
||||
|
||||
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
tmpdir := t.TempDir()
|
||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||
require.NoError(t, os.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
|
||||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2038-12-07T23:37:26.953313745Z")
|
||||
require.NoError(t, err)
|
||||
nowStr := now.Local().Format(time.RFC1123)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
env map[string]string
|
||||
loginErr error
|
||||
conciergeErr error
|
||||
wantError bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantOptionsCount int
|
||||
wantLogs []string
|
||||
}{
|
||||
{
|
||||
name: "help flag passed",
|
||||
args: []string{"--help"},
|
||||
wantStdout: here.Doc(`
|
||||
Login using a static token
|
||||
|
||||
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
|
||||
login command in its configuration. This login command is not meant to be
|
||||
invoked directly by a user.
|
||||
|
||||
This login command is a Kubernetes client-go credential plugin which is meant to
|
||||
be configured inside a kubeconfig file. (See the Kubernetes authentication
|
||||
documentation for more information about client-go credential plugins.)
|
||||
|
||||
Usage:
|
||||
static [--token TOKEN] [--token-env TOKEN_NAME] [flags]
|
||||
|
||||
Flags:
|
||||
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||
--concierge-authenticator-name string Concierge authenticator name
|
||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge
|
||||
--concierge-endpoint string API base for the Concierge endpoint
|
||||
--credential-cache string Path to cluster-specific credentials cache ("" disables the cache) (default "` + cfgDir + `/credentials.yaml")
|
||||
--enable-concierge Use the Concierge to login
|
||||
-h, --help help for static
|
||||
--token string Static token to present during login
|
||||
--token-env string Environment variable containing a static token
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing required flags",
|
||||
args: []string{},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: one of --token or --token-env must be set
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing concierge flags",
|
||||
args: []string{
|
||||
"--token", "test-token",
|
||||
"--enable-concierge",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: endpoint must not be empty
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing env var",
|
||||
args: []string{
|
||||
"--token-env", "TEST_TOKEN_ENV",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --token-env variable "TEST_TOKEN_ENV" is not set
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "empty env var",
|
||||
args: []string{
|
||||
"--token-env", "TEST_TOKEN_ENV",
|
||||
},
|
||||
env: map[string]string{
|
||||
"TEST_TOKEN_ENV": "",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --token-env variable "TEST_TOKEN_ENV" is empty
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "env var token success",
|
||||
args: []string{
|
||||
"--token-env", "TEST_TOKEN_ENV",
|
||||
},
|
||||
env: map[string]string{
|
||||
"TEST_TOKEN_ENV": "test-token",
|
||||
},
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"test-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "concierge failure",
|
||||
args: []string{
|
||||
"--token", "test-token",
|
||||
"--enable-concierge",
|
||||
"--concierge-endpoint", "https://127.0.0.1/",
|
||||
"--concierge-authenticator-type", "webhook",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
},
|
||||
conciergeErr: fmt.Errorf("some concierge error"),
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not complete Concierge credential exchange: some concierge error
|
||||
`),
|
||||
wantLogs: []string{
|
||||
nowStr + ` cmd/login_static.go:160 exchanging static token for cluster credential {"endpoint": "https://127.0.0.1/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid API group suffix",
|
||||
args: []string{
|
||||
"--token", "test-token",
|
||||
"--enable-concierge",
|
||||
"--concierge-api-group-suffix", ".starts.with.dot",
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "static token success",
|
||||
args: []string{
|
||||
"--token", "test-token",
|
||||
},
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true"},
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"test-token"}}` + "\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
ctx := plog.AddZapOverridesToContext(context.Background(), t, &buf, nil, clocktesting.NewFakeClock(now))
|
||||
|
||||
cmd := staticLoginCommand(staticLoginDeps{
|
||||
lookupEnv: func(s string) (string, bool) {
|
||||
v, ok := tt.env[s]
|
||||
return v, ok
|
||||
},
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
require.Equal(t, token, "test-token")
|
||||
if tt.conciergeErr != nil {
|
||||
return nil, tt.conciergeErr
|
||||
}
|
||||
return &clientauthv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ExecCredential",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: &clientauthv1beta1.ExecCredentialStatus{
|
||||
Token: "exchanged-token",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs(tt.args)
|
||||
err := cmd.ExecuteContext(ctx)
|
||||
if tt.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
||||
|
||||
require.Equal(t, tt.wantLogs, testutil.SplitByNewline(buf.String()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
)
|
||||
|
||||
// OIDCClientOptions is an interface that wraps the creation of Options for the purpose of making them
|
||||
// more friendly to unit tests. Because the Option type refers to a private struct type, it is hard
|
||||
// to create mocks for them in tests of other packages. This provides a seam that can be mocked.
|
||||
// No need for this interface to include deprecated options (such as WithLogger), since those should never be invoked.
|
||||
type OIDCClientOptions interface {
|
||||
WithContext(ctx context.Context) oidcclient.Option
|
||||
WithLoginLogger(logger oidcclient.Logger) oidcclient.Option
|
||||
WithListenPort(port uint16) oidcclient.Option
|
||||
WithSkipBrowserOpen() oidcclient.Option
|
||||
WithSkipListen() oidcclient.Option
|
||||
WithSkipPrintLoginURL() oidcclient.Option
|
||||
WithSessionCache(cache oidcclient.SessionCache) oidcclient.Option
|
||||
WithClient(httpClient *http.Client) oidcclient.Option
|
||||
WithScopes(scopes []string) oidcclient.Option
|
||||
WithRequestAudience(audience string) oidcclient.Option
|
||||
WithLoginFlow(loginFlow v1alpha1.IDPFlow, flowSource string) oidcclient.Option
|
||||
WithUpstreamIdentityProvider(upstreamName, upstreamType string) oidcclient.Option
|
||||
}
|
||||
|
||||
// clientOptions implements OIDCClientOptions for production use.
|
||||
type clientOptions struct{}
|
||||
|
||||
var _ OIDCClientOptions = (*clientOptions)(nil)
|
||||
|
||||
func (o *clientOptions) WithContext(ctx context.Context) oidcclient.Option {
|
||||
return oidcclient.WithContext(ctx)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithLoginLogger(logger oidcclient.Logger) oidcclient.Option {
|
||||
return oidcclient.WithLoginLogger(logger)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithListenPort(port uint16) oidcclient.Option {
|
||||
return oidcclient.WithListenPort(port)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithSkipBrowserOpen() oidcclient.Option {
|
||||
return oidcclient.WithSkipBrowserOpen()
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithSkipListen() oidcclient.Option {
|
||||
return oidcclient.WithSkipListen()
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithSkipPrintLoginURL() oidcclient.Option {
|
||||
return oidcclient.WithSkipPrintLoginURL()
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithSessionCache(cache oidcclient.SessionCache) oidcclient.Option {
|
||||
return oidcclient.WithSessionCache(cache)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithClient(httpClient *http.Client) oidcclient.Option {
|
||||
return oidcclient.WithClient(httpClient)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithScopes(scopes []string) oidcclient.Option {
|
||||
return oidcclient.WithScopes(scopes)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithRequestAudience(audience string) oidcclient.Option {
|
||||
return oidcclient.WithRequestAudience(audience)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithLoginFlow(loginFlow v1alpha1.IDPFlow, flowSource string) oidcclient.Option {
|
||||
return oidcclient.WithLoginFlow(loginFlow, flowSource)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithUpstreamIdentityProvider(upstreamName, upstreamType string) oidcclient.Option {
|
||||
return oidcclient.WithUpstreamIdentityProvider(upstreamName, upstreamType)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "pinniped",
|
||||
Long: here.Doc(
|
||||
`The Pinniped CLI is the client-side binary for use with Pinniped-enabled Kubernetes clusters
|
||||
|
||||
Find more information at: https://pinniped.dev`,
|
||||
),
|
||||
SilenceUsage: true, // do not print usage message when commands fail
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() error {
|
||||
defer plog.Setup()()
|
||||
// the context does not matter here because it is unused when CLI formatting is provided
|
||||
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(context.Background(), plog.LogSpec{Format: plog.FormatCLI}); err != nil {
|
||||
return err
|
||||
}
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user