mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-03-17 17:10:47 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2941f3f3ef | ||
|
|
98fb4be58f | ||
|
|
d0ec582334 | ||
|
|
e9099bdcf9 | ||
|
|
ff3f5e2444 | ||
|
|
5290aac66f | ||
|
|
4927f1c1ad | ||
|
|
e85bcca45f | ||
|
|
c1b1082c55 | ||
|
|
425e53a26b | ||
|
|
23cd53faeb | ||
|
|
24c8bdef44 | ||
|
|
4375c01afb | ||
|
|
91bf179b39 |
@@ -1,23 +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
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,3 +1,2 @@
|
||||
*.go.tmpl linguist-language=Go
|
||||
hack/Dockerfile_fips linguist-language=Dockerfile
|
||||
generated/** linguist-generated
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Feature request
|
||||
name: Feature proposal
|
||||
about: Suggest a way to improve this project
|
||||
title: ''
|
||||
labels: ''
|
||||
@@ -16,15 +16,12 @@ It is recommended that you include screenshots and logs to help everyone achieve
|
||||
-->
|
||||
|
||||
**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?**
|
||||
@@ -35,5 +32,4 @@ A clear and concise description of any alternative solutions or features you've
|
||||
- **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 -->
|
||||
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/
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -3,7 +3,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
open-pull-requests-limit: 100
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -12,8 +11,3 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- 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"
|
||||
|
||||
57
.github/workflows/codeql-analysis.yml
vendored
57
.github/workflows/codeql-analysis.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, release*, dynamic_clients ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main, release*, dynamic_clients ]
|
||||
schedule:
|
||||
- cron: '39 13 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
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.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# 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@v1
|
||||
|
||||
# ℹ️ 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
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
55
.github/workflows/scorecards.yml
vendored
55
.github/workflows/scorecards.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: Scorecards supply-chain security
|
||||
on:
|
||||
# Only the default branch is supported.
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: '29 11 * * 3'
|
||||
push:
|
||||
branches: [ main, release* ]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecards analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v3.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@c1aec4ac820532bab364f02a81873c555a0ba3a1 # v1.0.4
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# Read-only PAT token. To create it,
|
||||
# follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation.
|
||||
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
|
||||
# Publish the results to enable scorecard badges. For more details, see
|
||||
# https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories, `publish_results` will automatically be set to `false`,
|
||||
# regardless of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts (optional).
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
# 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~
|
||||
@@ -14,8 +11,11 @@
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# GoLand
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# goland
|
||||
.idea
|
||||
|
||||
# MacOS Desktop Services Store
|
||||
.DS_Store
|
||||
# Intermediate files used by Tilt
|
||||
/hack/lib/tilt/build
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
# https://github.com/golangci/golangci-lint#config-file
|
||||
run:
|
||||
deadline: 1m
|
||||
skip-dirs:
|
||||
- generated
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
# default linters
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
|
||||
# additional linters for this project (we should disable these if they get annoying).
|
||||
- asciicheck
|
||||
@@ -31,7 +32,7 @@ linters:
|
||||
- godot
|
||||
- goheader
|
||||
- goimports
|
||||
- revive
|
||||
- golint
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- misspell
|
||||
@@ -41,9 +42,10 @@ linters:
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- rowserrcheck
|
||||
- exportloopref
|
||||
- scopelint
|
||||
- sqlclosecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# 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)/'
|
||||
exclude: '^(generated|hack/lib/tilt/tilt_modules)/'
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
# TODO: find a version of this to validate ytt templates?
|
||||
@@ -11,7 +9,6 @@ repos:
|
||||
- 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
|
||||
|
||||
4
.pre-commit-hooks.yaml
Normal file
4
.pre-commit-hooks.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
- id: validate-copyright-year
|
||||
name: Validate copyright year
|
||||
entry: hack/check-copyright-year.sh
|
||||
language: script
|
||||
35
ADOPTERS.md
35
ADOPTERS.md
@@ -1,34 +1,9 @@
|
||||
# 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)!
|
||||
These organizations are using Pinniped.
|
||||
|
||||
## Organizations using Pinniped
|
||||
* [VMware Tanzu](https://tanzu.vmware.com/) ([Tanzu Mission Control](https://tanzu.vmware.com/mission-control))
|
||||
|
||||
<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-tanzu/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.
|
||||
If you are using Pinniped and are not on this list, you can open a [pull
|
||||
request](https://github.com/vmware-tanzu/pinniped/issues/new?template=feature-proposal.md)
|
||||
to add yourself.
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# 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
|
||||
@@ -11,19 +8,24 @@ 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.
|
||||
Learn about the [scope](https://pinniped.dev/docs/scope/) of the project.
|
||||
|
||||
## Roadmap
|
||||
## Meeting with the Maintainers
|
||||
|
||||
The near-term and mid-term roadmap for the work planned for the project [maintainers](MAINTAINERS.md) is documented in [ROADMAP.md](ROADMAP.md).
|
||||
The maintainers aspire to hold a video conference every other week with the Pinniped community.
|
||||
Any community member may request to add topics to the agenda by contacting a [maintainer](MAINTAINERS.md)
|
||||
in advance, or by attending and raising the topic during time remaining after the agenda is covered.
|
||||
Typical agenda items include topics regarding the roadmap, feature requests, bug reports, pull requests, etc.
|
||||
A [public document](https://docs.google.com/document/d/1qYA35wZV-6bxcH5375vOnIGkNBo7e4OROgsV4Sj8WjQ)
|
||||
tracks the agendas and notes for these meetings.
|
||||
|
||||
These meetings are currently scheduled for the first and third Thursday mornings of each month
|
||||
at 9 AM Pacific Time, using this [Zoom meeting](https://VMware.zoom.us/j/94638309756?pwd=V3NvRXJIdDg5QVc0TUdFM2dYRzgrUT09).
|
||||
If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled.
|
||||
|
||||
## Discussion
|
||||
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out
|
||||
via GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions),
|
||||
GitHub [Issues](https://github.com/vmware-tanzu/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.
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page.
|
||||
|
||||
## Issues
|
||||
|
||||
@@ -50,7 +52,7 @@ guidelines in the issue and pull request templates.
|
||||
To suggest a feature, please first open an
|
||||
[issue](https://github.com/vmware-tanzu/pinniped/issues/new?template=feature-proposal.md)
|
||||
and tag it with `proposal`, or create a new [Discussion](https://github.com/vmware-tanzu/pinniped/discussions).
|
||||
The project [maintainers](MAINTAINERS.md) will work with you on your feature request.
|
||||
The project team will work with you on your feature request.
|
||||
|
||||
Once the feature request has been validated, a [pull request](https://github.com/vmware-tanzu/pinniped/compare)
|
||||
can be opened to implement the feature.
|
||||
@@ -58,44 +60,25 @@ 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
|
||||
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.
|
||||
package the 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
|
||||
@@ -120,35 +103,36 @@ go build -o pinniped ./cmd/pinniped
|
||||
- [`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/)
|
||||
- [`tilt`](https://docs.tilt.dev/install.html)
|
||||
- [`ytt`](https://carvel.dev/#getting-started)
|
||||
- [`nmap`](https://nmap.org/download.html)
|
||||
|
||||
On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
|
||||
|
||||
```bash
|
||||
brew install kind vmware-tanzu/carvel/ytt vmware-tanzu/carvel/kapp kubectl chromedriver nmap && brew cask install docker
|
||||
brew install kind tilt-dev/tap/tilt k14s/tap/ytt k14s/tap/kapp kubectl chromedriver && brew cask install docker
|
||||
```
|
||||
|
||||
1. Create a kind cluster, compile, create container images, and install Pinniped and supporting test dependencies using:
|
||||
1. Create a local Kubernetes cluster using `kind`:
|
||||
|
||||
```bash
|
||||
./hack/prepare-for-integration-tests.sh
|
||||
./hack/kind-up.sh
|
||||
```
|
||||
|
||||
1. Install Pinniped and supporting dependencies using `tilt`:
|
||||
|
||||
```bash
|
||||
./hack/tilt-up.sh
|
||||
```
|
||||
|
||||
Tilt will continue running and live-updating the Pinniped deployment whenever the code changes.
|
||||
|
||||
1. Run the Pinniped integration tests:
|
||||
|
||||
```bash
|
||||
ulimit -n 512 && source /tmp/integration-test-env && go test -v -count 1 -timeout 0 ./test/integration
|
||||
source /tmp/integration-test-env && go test -v -count 1 ./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-tanzu/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 uninstall the test environment, run `./hack/tilt-down.sh`.
|
||||
To destroy the local Kubernetes cluster, run `./hack/kind-down.sh`.
|
||||
|
||||
### Observing Tests on the Continuous Integration Environment
|
||||
|
||||
57
Dockerfile
57
Dockerfile
@@ -1,41 +1,42 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.19.5 as build-env
|
||||
FROM golang:1.15.12 as build-env
|
||||
|
||||
WORKDIR /work
|
||||
COPY . .
|
||||
ARG GOPROXY
|
||||
# Get dependencies first so they can be cached as a layer
|
||||
COPY go.* ./
|
||||
COPY generated/1.20/apis/go.* ./generated/1.20/apis/
|
||||
COPY generated/1.20/client/go.* ./generated/1.20/client/
|
||||
RUN go mod download
|
||||
|
||||
# Copy only the production source code to avoid cache misses when editing other files
|
||||
COPY generated ./generated
|
||||
COPY cmd ./cmd
|
||||
COPY pkg ./pkg
|
||||
COPY internal ./internal
|
||||
COPY hack ./hack
|
||||
|
||||
# Build the executable binary (CGO_ENABLED=0 means static linking)
|
||||
# Pass in GOCACHE (build cache) and GOMODCACHE (module cache) so they
|
||||
# can be re-used between image builds.
|
||||
RUN \
|
||||
--mount=type=cache,target=/cache/gocache \
|
||||
--mount=type=cache,target=/cache/gomodcache \
|
||||
mkdir out && \
|
||||
export GOCACHE=/cache/gocache GOMODCACHE=/cache/gomodcache CGO_ENABLED=0 GOOS=linux GOARCH=amd64 && \
|
||||
go build -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 -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
|
||||
RUN mkdir out \
|
||||
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-concierge/... \
|
||||
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-supervisor/... \
|
||||
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out ./cmd/local-user-authenticator/...
|
||||
|
||||
# Use a distroless runtime image with CA certificates, timezone data, and not much else.
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:2a9e2b4fa771d31fe3346a873be845bfc2159695b9f90ca08e950497006ccc2e
|
||||
# Use a runtime image based on Debian slim
|
||||
FROM debian:10.9-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates procps && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the server binary from the build-env stage.
|
||||
COPY --from=build-env /usr/local/bin /usr/local/bin
|
||||
# Copy the binaries from the build-env stage
|
||||
COPY --from=build-env /work/out/pinniped-concierge /usr/local/bin/pinniped-concierge
|
||||
COPY --from=build-env /work/out/pinniped-supervisor /usr/local/bin/pinniped-supervisor
|
||||
COPY --from=build-env /work/out/local-user-authenticator /usr/local/bin/local-user-authenticator
|
||||
|
||||
# Document the default server ports for the various server apps
|
||||
EXPOSE 8443 8444 10250
|
||||
# Document the ports
|
||||
EXPOSE 8080 8443
|
||||
|
||||
# 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
|
||||
USER 1001:1001
|
||||
|
||||
# Set the entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/pinniped-server"]
|
||||
ENTRYPOINT ["/usr/local/bin/pinniped-concierge"]
|
||||
|
||||
@@ -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-tanzu/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-tanzu/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,24 +1,19 @@
|
||||
# Current Pinniped Maintainers
|
||||
# Pinniped Maintainers
|
||||
|
||||
| Maintainer | GitHub ID | Affiliation |
|
||||
|-----------------|-----------------------------------------------------------|------------------------------------------|
|
||||
| Ben Petersen | [benjaminapetersen](https://github.com/benjaminapetersen) | [VMware](https://www.github.com/vmware/) |
|
||||
| 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/) |
|
||||
This is the current list of maintainers for the Pinniped project.
|
||||
|
||||
## Emeritus Maintainers
|
||||
| Maintainer | GitHub ID | Affiliation |
|
||||
| --------------- | --------- | ----------- |
|
||||
| Andrew Keesler | [ankeesler](https://github.com/ankeesler) | [VMware](https://www.github.com/vmware/) |
|
||||
| Margo Crawford | [margocrawf](https://github.com/margocrawf) | [VMware](https://www.github.com/vmware/) |
|
||||
| Matt Moyer | [mattmoyer](https://github.com/mattmoyer) | [VMware](https://www.github.com/vmware/) |
|
||||
| Mo Khan | [enj](https://github.com/enj) | [VMware](https://www.github.com/vmware/) |
|
||||
| Pablo Schuhmacher | [pabloschuhmacher](https://github.com/pabloschuhmacher) | [VMware](https://www.github.com/vmware/) |
|
||||
| Ryan Richard | [cfryanr](https://github.com/cfryanr) | [VMware](https://www.github.com/vmware/) |
|
||||
|
||||
| Maintainer | GitHub ID |
|
||||
|-------------------|---------------------------------------------------------|
|
||||
| Andrew Keesler | [ankeesler](https://github.com/ankeesler) |
|
||||
| Anjali Telang | [anjaltelang](https://github.com/anjaltelang) |
|
||||
| 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) |
|
||||
## Pinniped Contributors & Stakeholders
|
||||
|
||||
## Pinniped Community Management
|
||||
|
||||
| Community Manager | GitHub ID |
|
||||
|-------------------|---------------------------------------|
|
||||
| Nigel Brown | [pnbrown](https://github.com/pnbrown) |
|
||||
| Feature Area | Lead |
|
||||
| ----------------------------- | :---------------------: |
|
||||
| Technical Lead | Matt Moyer (mattmoyer) |
|
||||
| Product Management | Pablo Schuhmacher (pabloschuhmacher) |
|
||||
|
||||
72
README.md
72
README.md
@@ -1,45 +1,61 @@
|
||||
<a href="https://pinniped.dev" target="_blank">
|
||||
<img src="site/content/docs/img/pinniped_logo.svg" alt="Pinniped Logo" width="100%"/>
|
||||
</a>
|
||||
<img src="site/content/docs/img/pinniped_logo.svg" alt="Pinniped Logo" width="100%"/>
|
||||
|
||||
## Overview
|
||||
|
||||
Pinniped provides identity services to Kubernetes.
|
||||
|
||||
- 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.
|
||||
Pinniped allows cluster administrators to easily plug in external identity
|
||||
providers (IDPs) into Kubernetes clusters. This is achieved via a uniform
|
||||
install procedure across all types and origins of Kubernetes clusters,
|
||||
declarative configuration via Kubernetes APIs, enterprise-grade integrations
|
||||
with IDPs, and distribution-specific integration strategies.
|
||||
|
||||
To learn more, please visit the Pinniped project's website, https://pinniped.dev.
|
||||
### Example Use Cases
|
||||
|
||||
## Getting started with Pinniped
|
||||
* Your team uses a large enterprise IDP, and has many clusters that they
|
||||
manage. Pinniped provides:
|
||||
* Seamless and robust integration with the IDP
|
||||
* Easy installation across clusters of any type and origin
|
||||
* A simplified login flow across all clusters
|
||||
* Your team shares a single cluster. Pinniped provides:
|
||||
* Simple configuration to integrate an IDP
|
||||
* Individual, revocable identities
|
||||
|
||||
Care to kick the tires? It's easy to [install and try Pinniped](https://pinniped.dev/docs/).
|
||||
### Architecture
|
||||
|
||||
The Pinniped Supervisor component offers identity federation to enable a user to
|
||||
access multiple clusters with a single daily login to their external IDP. The
|
||||
Pinniped Supervisor supports various external [IDP
|
||||
types](https://github.com/vmware-tanzu/pinniped/tree/main/generated/1.20#k8s-api-idp-supervisor-pinniped-dev-v1alpha1).
|
||||
|
||||
The Pinniped Concierge component offers credential exchange to enable a user to
|
||||
exchange an external credential for a short-lived, cluster-specific
|
||||
credential. Pinniped supports various [authentication
|
||||
methods](https://github.com/vmware-tanzu/pinniped/tree/main/generated/1.20#authenticationconciergepinnipeddevv1alpha1)
|
||||
and implements different integration strategies for various Kubernetes
|
||||
distributions to make authentication possible.
|
||||
|
||||
The Pinniped Concierge can be configured to hook into the Pinniped Supervisor's
|
||||
federated credentials, or it can authenticate users directly via external IDP
|
||||
credentials.
|
||||
|
||||
To learn more, see [architecture](https://pinniped.dev/docs/architecture/).
|
||||
|
||||
<img src="site/content/docs/img/pinniped_architecture_concierge_supervisor.svg" alt="Pinniped Architecture Sketch"/>
|
||||
|
||||
## Trying Pinniped
|
||||
|
||||
Care to kick the tires? It's easy to [install and try Pinniped](https://pinniped.dev/docs/demo/).
|
||||
|
||||
## Discussion
|
||||
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out
|
||||
via GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions),
|
||||
GitHub [Issues](https://github.com/vmware-tanzu/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.
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page.
|
||||
|
||||
## 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.
|
||||
Contributions are welcome. Before contributing, please see the [contributing guide](CONTRIBUTING.md).
|
||||
|
||||
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-tanzu/pinniped/discussions/152).
|
||||
|
||||
## Reporting security vulnerabilities
|
||||
## Reporting Security Vulnerabilities
|
||||
|
||||
Please follow the procedure described in [SECURITY.md](SECURITY.md).
|
||||
|
||||
@@ -47,4 +63,4 @@ Please follow the procedure described in [SECURITY.md](SECURITY.md).
|
||||
|
||||
Pinniped is open source and licensed under Apache License Version 2.0. See [LICENSE](LICENSE).
|
||||
|
||||
Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
|
||||
41
ROADMAP.md
41
ROADMAP.md
@@ -1,41 +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.
|
||||
The [Pinniped project backlog](https://github.com/orgs/vmware-tanzu/projects/43/) is prioritized based on this roadmap,
|
||||
and it provides a more granular view of what the maintainers are working on a day-to-day basis.
|
||||
|
||||
### 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-tanzu/pinniped#discussion).
|
||||
[Contributions](https://github.com/vmware-tanzu/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-tanzu/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.
|
||||
|
||||
### Current Roadmap
|
||||
|
||||
The following table includes the current roadmap for Pinniped. Please take the timelines and dates as proposals and
|
||||
goals. 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-tanzu/pinniped/blob/main/CONTRIBUTING.md) to Pinniped.
|
||||
|
||||
Last Updated: Sept 2022
|
||||
|Theme|Description|Timeline|
|
||||
|--|--|--|
|
||||
|Improving Usability|Dynamic Oauth Client Support for integrating with UI/Dashboards |Sept/Oct 2022|
|
||||
|Improving Usability|Support for custom claim mappings in OIDCIdentityProvider |Q4 2022|
|
||||
|Improving Usability|Support for Multiple Identity Providers |Q4 2022|
|
||||
|Improving Security Posture|Support Audit logging of security events related to Authentication |Q4 2022|
|
||||
|Improving Security Posture|Session Management |2022/2023|
|
||||
|Improving Security Posture|Secrets Rotation and Management |2022/2023|
|
||||
32
SCOPE.md
32
SCOPE.md
@@ -1,32 +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.
|
||||
|
||||
## Roadmap
|
||||
|
||||
See our [open milestones][milestones] and the [`priority/backlog` label][backlog] for an idea about what's next on our roadmap.
|
||||
|
||||
For more details on proposing features and bugs, check out our [contributing](./CONTRIBUTING.md) doc.
|
||||
|
||||
[milestones]: https://github.com/vmware-tanzu/pinniped/milestones
|
||||
[backlog]: https://github.com/vmware-tanzu/pinniped/labels/priority%2Fbacklog
|
||||
100
SECURITY.md
100
SECURITY.md
@@ -1,92 +1,12 @@
|
||||
# Security Release Process
|
||||
# Reporting a Vulnerability
|
||||
|
||||
Pinniped provides identity services for Kubernetes clusters. The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues.
|
||||
Pinniped development is sponsored by VMware, and the Pinniped team encourages users
|
||||
who become aware of a security vulnerability in Pinniped to report any potential
|
||||
vulnerabilities found to security@vmware.com. If possible, please include a description
|
||||
of the effects of the vulnerability, reproduction steps, and a description of in which
|
||||
version of Pinniped or its dependencies the vulnerability was discovered.
|
||||
The use of encrypted email is encouraged. The public PGP key can be found at https://kb.vmware.com/kb/1055.
|
||||
|
||||
## 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 (security@vmware.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-tanzu/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-tanzu/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 security@vmware.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 (security@vmware.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.
|
||||
The Pinniped team hopes that users encountering a new vulnerability will contact
|
||||
us privately as it is in the best interests of our users that the Pinniped team has
|
||||
an opportunity to investigate and confirm a suspected vulnerability before it becomes public knowledge.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -57,13 +57,9 @@ type JWTTokenClaims struct {
|
||||
// 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:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators
|
||||
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
|
||||
// +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type JWTAuthenticator struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -29,12 +29,9 @@ type WebhookAuthenticatorSpec struct {
|
||||
|
||||
// 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:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators
|
||||
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type WebhookAuthenticator struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,144 +1,40 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
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
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
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
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
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")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
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.
|
||||
//
|
||||
// +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.
|
||||
//
|
||||
// +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")
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Status of a credential issuer.
|
||||
type CredentialIssuerStatus struct {
|
||||
// List of integration strategies that were attempted by Pinniped.
|
||||
Strategies []CredentialIssuerStrategy `json:"strategies"`
|
||||
|
||||
// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer.
|
||||
// This field is deprecated and will be removed in a future version.
|
||||
// +optional
|
||||
KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"`
|
||||
}
|
||||
|
||||
// CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer.
|
||||
// This type is deprecated and will be removed in a future version.
|
||||
// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer.
|
||||
type CredentialIssuerKubeConfigInfo struct {
|
||||
// The K8s API server URL.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
@@ -150,7 +46,7 @@ type CredentialIssuerKubeConfigInfo struct {
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
||||
// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped.
|
||||
// Status of an integration strategy that was attempted by Pinniped.
|
||||
type CredentialIssuerStrategy struct {
|
||||
// Type of integration attempted.
|
||||
Type StrategyType `json:"type"`
|
||||
@@ -167,74 +63,21 @@ type CredentialIssuerStrategy struct {
|
||||
|
||||
// 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.
|
||||
// Describes the configuration status of a Pinniped 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
|
||||
// +kubebuilder:resource:categories=pinniped
|
||||
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 of the credential issuer.
|
||||
Status CredentialIssuerStatus `json:"status"`
|
||||
}
|
||||
|
||||
// CredentialIssuerList is a list of CredentialIssuer objects.
|
||||
// List of CredentialIssuer objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type CredentialIssuerList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2021-2022 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-2022 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-2022 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-2022 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-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2021-2022 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,11 +0,0 @@
|
||||
// Copyright 2021-2022 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
|
||||
// +groupName=identity.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped identity API.
|
||||
package v1alpha1
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright 2021-2022 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-2022 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-2022 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-2022 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,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package login
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 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.
|
||||
// ClusterCredential is a credential (token or certificate) which is valid on the Kubernetes cluster.
|
||||
type ClusterCredential struct {
|
||||
// ExpirationTimestamp indicates a time when the provided credentials expire.
|
||||
ExpirationTimestamp metav1.Time
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package login
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
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
|
||||
@@ -17,9 +16,8 @@ type TokenCredentialRequestSpec struct {
|
||||
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.
|
||||
// A ClusterCredential will be returned for a successful credential request.
|
||||
// +optional
|
||||
Credential *ClusterCredential
|
||||
|
||||
@@ -29,6 +27,7 @@ type TokenCredentialRequestStatus struct {
|
||||
}
|
||||
|
||||
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequest struct {
|
||||
metav1.TypeMeta
|
||||
@@ -44,6 +43,6 @@ type TokenCredentialRequestList struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ListMeta
|
||||
|
||||
// Items is a list of TokenCredentialRequest.
|
||||
// Items is a list of TokenCredentialRequest
|
||||
Items []TokenCredentialRequest
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Specification of a TokenCredentialRequest, expected on requests to the Pinniped API.
|
||||
// TokenCredentialRequestSpec is the 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"`
|
||||
@@ -17,7 +17,7 @@ type TokenCredentialRequestSpec struct {
|
||||
Authenticator corev1.TypedLocalObjectReference `json:"authenticator"`
|
||||
}
|
||||
|
||||
// Status of a TokenCredentialRequest, returned on responses to the Pinniped API.
|
||||
// TokenCredentialRequestStatus is the 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
|
||||
@@ -30,8 +30,6 @@ type TokenCredentialRequestStatus struct {
|
||||
|
||||
// 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"`
|
||||
@@ -47,6 +45,5 @@ 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 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 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 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 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2022 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,11 +0,0 @@
|
||||
// Copyright 2022 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
|
||||
// +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 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 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,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -32,8 +32,6 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&FederationDomain{},
|
||||
&FederationDomainList{},
|
||||
&OIDCClient{},
|
||||
&OIDCClientList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -31,9 +31,8 @@ type FederationDomainTLSSpec struct {
|
||||
// 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
|
||||
// SecretName is not required when you would like to use only the HTTP endpoints (e.g. when terminating TLS at an
|
||||
// Ingress). 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.
|
||||
@@ -110,10 +109,6 @@ type FederationDomainStatus struct {
|
||||
// +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.status`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type FederationDomain struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// ConditionStatus is effectively an enum type for Condition.Status.
|
||||
type ConditionStatus string
|
||||
|
||||
// These are valid condition statuses. "ConditionTrue" means a resource is in the condition.
|
||||
// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes
|
||||
// can't decide if a resource is in the condition or not. In the future, we could add other
|
||||
// intermediate conditions, e.g. ConditionDegraded.
|
||||
const (
|
||||
ConditionTrue ConditionStatus = "True"
|
||||
ConditionFalse ConditionStatus = "False"
|
||||
ConditionUnknown ConditionStatus = "Unknown"
|
||||
)
|
||||
|
||||
// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API
|
||||
// version we can switch to using the upstream type.
|
||||
// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
|
||||
type Condition struct {
|
||||
// type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
// ---
|
||||
// Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
|
||||
// useful (see .node.status.conditions), the ability to deconflict is important.
|
||||
// The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$`
|
||||
// +kubebuilder:validation:MaxLength=316
|
||||
Type string `json:"type"`
|
||||
|
||||
// status of the condition, one of True, False, Unknown.
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:Enum=True;False;Unknown
|
||||
Status ConditionStatus `json:"status"`
|
||||
|
||||
// observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
// For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
// with respect to the current state of the instance.
|
||||
// +optional
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
|
||||
// lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
// This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:Type=string
|
||||
// +kubebuilder:validation:Format=date-time
|
||||
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
|
||||
|
||||
// reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
// Producers of specific condition types may define expected values and meanings for this field,
|
||||
// and whether the values are considered a guaranteed API.
|
||||
// The value should be a CamelCase string.
|
||||
// This field may not be empty.
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MaxLength=1024
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$`
|
||||
Reason string `json:"reason"`
|
||||
|
||||
// message is a human readable message indicating details about the transition.
|
||||
// This may be an empty string.
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MaxLength=32768
|
||||
Message string `json:"message"`
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
// Copyright 2022 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 (
|
||||
// PhasePending is the default phase for newly-created OIDCClient resources.
|
||||
PhasePending OIDCClientPhase = "Pending"
|
||||
|
||||
// PhaseReady is the phase for an OIDCClient resource in a healthy state.
|
||||
PhaseReady OIDCClientPhase = "Ready"
|
||||
|
||||
// PhaseError is the phase for an OIDCClient in an unhealthy state.
|
||||
PhaseError 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"`
|
||||
}
|
||||
|
||||
// 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 []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,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -32,10 +32,6 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&OIDCIdentityProvider{},
|
||||
&OIDCIdentityProviderList{},
|
||||
&LDAPIdentityProvider{},
|
||||
&LDAPIdentityProviderList{},
|
||||
&ActiveDirectoryIdentityProvider{},
|
||||
&ActiveDirectoryIdentityProviderList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
// Copyright 2021-2022 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 []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
|
||||
// dn (distinguished name) of the user entry found as a result of the user search. 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"`
|
||||
|
||||
// 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,196 +0,0 @@
|
||||
// Copyright 2021-2022 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 []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 and Attributes 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
|
||||
// dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or
|
||||
// "&(objectClass=groupOfNames)(member={})". 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"`
|
||||
|
||||
// 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,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -20,7 +20,7 @@ const (
|
||||
PhaseError OIDCIdentityProviderPhase = "Error"
|
||||
)
|
||||
|
||||
// OIDCIdentityProviderStatus is the status of an OIDC identity provider.
|
||||
// Status of an OIDC identity provider.
|
||||
type OIDCIdentityProviderStatus struct {
|
||||
// Phase summarizes the overall status of the OIDCIdentityProvider.
|
||||
// +kubebuilder:default=Pending
|
||||
@@ -38,117 +38,23 @@ type OIDCIdentityProviderStatus struct {
|
||||
// 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).
|
||||
// AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization
|
||||
// request flow with an OIDC identity provider. By default only the "openid" scope will be requested.
|
||||
// +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.
|
||||
// Groups provides the name of the token claim that will be used to ascertain the groups to which
|
||||
// an identity belongs.
|
||||
// +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.
|
||||
// Username provides the name of the token claim that will be used to ascertain an identity's
|
||||
// username.
|
||||
// +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
|
||||
@@ -161,7 +67,7 @@ type OIDCClient struct {
|
||||
SecretName string `json:"secretName"`
|
||||
}
|
||||
|
||||
// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
|
||||
// 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.
|
||||
@@ -207,7 +113,7 @@ type OIDCIdentityProvider struct {
|
||||
Status OIDCIdentityProviderStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCIdentityProviderList lists OIDCIdentityProvider objects.
|
||||
// List of OIDCIdentityProvider objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCIdentityProviderList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright 2021-2022 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"
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright 2021-2023 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"
|
||||
|
||||
// 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,13 +1,13 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package localuserauthenticator provides a authentication webhook program.
|
||||
// Package main provides a authentication webhook program.
|
||||
//
|
||||
// This webhook is meant to be used in demo settings to play around with
|
||||
// Pinniped. As well, it can come in handy in integration tests.
|
||||
//
|
||||
// This webhook is NOT meant for use in production systems.
|
||||
package localuserauthenticator
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -32,11 +31,11 @@ import (
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/controller/apicerts"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/crypto/ptls"
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
@@ -55,12 +54,12 @@ const (
|
||||
)
|
||||
|
||||
type webhook struct {
|
||||
certProvider dynamiccert.Private
|
||||
certProvider dynamiccert.Provider
|
||||
secretInformer corev1informers.SecretInformer
|
||||
}
|
||||
|
||||
func newWebhook(
|
||||
certProvider dynamiccert.Private,
|
||||
certProvider dynamiccert.Provider,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
) *webhook {
|
||||
return &webhook{
|
||||
@@ -72,16 +71,16 @@ func newWebhook(
|
||||
// start runs the webhook in a separate goroutine and returns whether or not the
|
||||
// webhook was started successfully.
|
||||
func (w *webhook) start(ctx context.Context, l net.Listener) error {
|
||||
c := ptls.Secure(nil)
|
||||
c.GetCertificate = func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
certPEM, keyPEM := w.certProvider.CurrentCertKeyContent()
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
return &cert, err
|
||||
}
|
||||
server := http.Server{
|
||||
Handler: w,
|
||||
TLSConfig: c,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
Handler: w,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
certPEM, keyPEM := w.certProvider.CurrentCertKeyContent()
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
return &cert, err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
@@ -151,7 +150,7 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
plog.Debug("successful authentication")
|
||||
respondWithAuthenticated(rsp, secret.ObjectMeta.Name, groups)
|
||||
respondWithAuthenticated(rsp, secret.ObjectMeta.Name, string(secret.UID), groups)
|
||||
}
|
||||
|
||||
func getUsernameAndPasswordFromRequest(rsp http.ResponseWriter, req *http.Request) (string, string, error) {
|
||||
@@ -256,7 +255,7 @@ func respondWithUnauthenticated(rsp http.ResponseWriter) {
|
||||
|
||||
func respondWithAuthenticated(
|
||||
rsp http.ResponseWriter,
|
||||
username string,
|
||||
username, uid string,
|
||||
groups []string,
|
||||
) {
|
||||
rsp.Header().Add("Content-Type", "application/json")
|
||||
@@ -270,6 +269,7 @@ func respondWithAuthenticated(
|
||||
User: authenticationv1beta1.UserInfo{
|
||||
Username: username,
|
||||
Groups: groups,
|
||||
UID: uid,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -281,7 +281,7 @@ func respondWithAuthenticated(
|
||||
|
||||
func startControllers(
|
||||
ctx context.Context,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
kubeClient kubernetes.Interface,
|
||||
kubeInformers kubeinformers.SharedInformerFactory,
|
||||
) {
|
||||
@@ -328,7 +328,7 @@ func startControllers(
|
||||
func startWebhook(
|
||||
ctx context.Context,
|
||||
l net.Listener,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
) error {
|
||||
return newWebhook(dynamicCertProvider, secretInformer).start(ctx, l)
|
||||
@@ -340,7 +340,10 @@ func waitForSignal() os.Signal {
|
||||
return <-signalCh
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
func run() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
client, err := kubeclient.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create k8s client: %w", err)
|
||||
@@ -352,12 +355,12 @@ func run(ctx context.Context) error {
|
||||
kubeinformers.WithNamespace(namespace),
|
||||
)
|
||||
|
||||
dynamicCertProvider := dynamiccert.NewServingCert("local-user-authenticator-tls-serving-certificate")
|
||||
dynamicCertProvider := dynamiccert.New()
|
||||
|
||||
startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers)
|
||||
plog.Debug("controllers are ready")
|
||||
|
||||
//nolint:gosec // Intentionally binding to all network interfaces.
|
||||
//nolint: gosec // Intentionally binding to all network interfaces.
|
||||
l, err := net.Listen("tcp", ":8443")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create listener: %w", err)
|
||||
@@ -376,35 +379,13 @@ func run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() error { // return an error instead of plog.Fatal to allow defer statements to run
|
||||
ctx := signalCtx()
|
||||
|
||||
func main() {
|
||||
// Hardcode the logging level to debug, since this is a test app and it is very helpful to have
|
||||
// verbose logs to debug test failures.
|
||||
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug}); err != nil {
|
||||
plog.Fatal(err)
|
||||
if err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
|
||||
return run(ctx)
|
||||
}
|
||||
|
||||
func Main() {
|
||||
if err := main(); err != nil {
|
||||
plog.Fatal(err)
|
||||
if err := run(); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func signalCtx() context.Context {
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
defer cancel()
|
||||
|
||||
s := <-signalCh
|
||||
plog.Debug("saw signal", "signal", s)
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package localuserauthenticator
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -23,7 +26,7 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -31,7 +34,6 @@ import (
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
"go.pinniped.dev/internal/net/phttp"
|
||||
)
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
@@ -44,6 +46,8 @@ func TestWebhook(t *testing.T) {
|
||||
|
||||
user, otherUser, colonUser, noGroupUser, oneGroupUser, passwordUndefinedUser, emptyPasswordUser, invalidPasswordHashUser, undefinedGroupsUser :=
|
||||
"some-user", "other-user", "colon-user", "no-group-user", "one-group-user", "password-undefined-user", "empty-password-user", "invalid-password-hash-user", "undefined-groups-user"
|
||||
uid, otherUID, colonUID, noGroupUID, oneGroupUID, passwordUndefinedUID, emptyPasswordUID, invalidPasswordHashUID, undefinedGroupsUID :=
|
||||
"some-uid", "other-uid", "colon-uid", "no-group-uid", "one-group-uid", "password-undefined-uid", "empty-password-uid", "invalid-password-hash-uid", "undefined-groups-uid"
|
||||
password, otherPassword, colonPassword, noGroupPassword, oneGroupPassword, undefinedGroupsPassword :=
|
||||
"some-password", "other-password", "some-:-password", "no-group-password", "one-group-password", "undefined-groups-password"
|
||||
|
||||
@@ -51,15 +55,16 @@ func TestWebhook(t *testing.T) {
|
||||
groups := group0 + " , " + group1
|
||||
|
||||
kubeClient := kubernetesfake.NewSimpleClientset()
|
||||
addSecretToFakeClientTracker(t, kubeClient, user, password, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, otherUser, otherPassword, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, colonUser, colonPassword, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, noGroupUser, noGroupPassword, "")
|
||||
addSecretToFakeClientTracker(t, kubeClient, oneGroupUser, oneGroupPassword, group0)
|
||||
addSecretToFakeClientTracker(t, kubeClient, emptyPasswordUser, "", groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, user, uid, password, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, otherUser, otherUID, otherPassword, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, colonUser, colonUID, colonPassword, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, noGroupUser, noGroupUID, noGroupPassword, "")
|
||||
addSecretToFakeClientTracker(t, kubeClient, oneGroupUser, oneGroupUID, oneGroupPassword, group0)
|
||||
addSecretToFakeClientTracker(t, kubeClient, emptyPasswordUser, emptyPasswordUID, "", groups)
|
||||
|
||||
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(passwordUndefinedUID),
|
||||
Name: passwordUndefinedUser,
|
||||
Namespace: namespace,
|
||||
},
|
||||
@@ -73,6 +78,7 @@ func TestWebhook(t *testing.T) {
|
||||
|
||||
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(undefinedGroupsUID),
|
||||
Name: undefinedGroupsUser,
|
||||
Namespace: namespace,
|
||||
},
|
||||
@@ -83,6 +89,7 @@ func TestWebhook(t *testing.T) {
|
||||
|
||||
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(invalidPasswordHashUID),
|
||||
Name: invalidPasswordHashUser,
|
||||
Namespace: namespace,
|
||||
},
|
||||
@@ -92,7 +99,7 @@ func TestWebhook(t *testing.T) {
|
||||
},
|
||||
}))
|
||||
|
||||
secretInformer := createSecretInformer(ctx, t, kubeClient)
|
||||
secretInformer := createSecretInformer(t, kubeClient)
|
||||
|
||||
certProvider, caBundle, serverName := newCertProvider(t)
|
||||
w := newWebhook(certProvider, secretInformer)
|
||||
@@ -102,7 +109,7 @@ func TestWebhook(t *testing.T) {
|
||||
defer func() { _ = l.Close() }()
|
||||
require.NoError(t, w.start(ctx, l))
|
||||
|
||||
client := newClient(t, caBundle, serverName)
|
||||
client := newClient(caBundle, serverName)
|
||||
|
||||
goodURL := fmt.Sprintf("https://%s/authenticate", l.Addr().String())
|
||||
goodRequestHeaders := map[string][]string{
|
||||
@@ -129,7 +136,7 @@ func TestWebhook(t *testing.T) {
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(user, []string{group0, group1}),
|
||||
wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "success for a user who belongs to one groups",
|
||||
@@ -139,7 +146,7 @@ func TestWebhook(t *testing.T) {
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(oneGroupUser + ":" + oneGroupPassword) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(oneGroupUser, []string{group0}),
|
||||
wantBody: authenticatedResponseJSON(oneGroupUser, oneGroupUID, []string{group0}),
|
||||
},
|
||||
{
|
||||
name: "success for a user who belongs to no groups",
|
||||
@@ -149,7 +156,7 @@ func TestWebhook(t *testing.T) {
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(noGroupUser + ":" + noGroupPassword) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(noGroupUser, nil),
|
||||
wantBody: authenticatedResponseJSON(noGroupUser, noGroupUID, nil),
|
||||
},
|
||||
{
|
||||
name: "wrong username for password",
|
||||
@@ -191,7 +198,7 @@ func TestWebhook(t *testing.T) {
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(undefinedGroupsUser, nil),
|
||||
wantBody: authenticatedResponseJSON(undefinedGroupsUser, undefinedGroupsUID, nil),
|
||||
},
|
||||
{
|
||||
name: "when a user has empty string as their password",
|
||||
@@ -249,7 +256,7 @@ func TestWebhook(t *testing.T) {
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(colonUser + ":" + colonPassword) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(colonUser, []string{group0, group1}),
|
||||
wantBody: authenticatedResponseJSON(colonUser, colonUID, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "bad TokenReview group",
|
||||
@@ -351,7 +358,7 @@ func TestWebhook(t *testing.T) {
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(user, []string{group0, group1}),
|
||||
wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "success when there are multiple accepts and one of them is */*",
|
||||
@@ -364,7 +371,7 @@ func TestWebhook(t *testing.T) {
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(user, []string{group0, group1}),
|
||||
wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "success when there are multiple accepts and one of them is application/*",
|
||||
@@ -377,14 +384,14 @@ func TestWebhook(t *testing.T) {
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(user, []string{group0, group1}),
|
||||
wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "bad body",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewBuffer([]byte("invalid body"))), nil },
|
||||
body: func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewBuffer([]byte("invalid body"))), nil },
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
@@ -415,7 +422,7 @@ func TestWebhook(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
responseBody, err := io.ReadAll(rsp.Body)
|
||||
responseBody, err := ioutil.ReadAll(rsp.Body)
|
||||
require.NoError(t, err)
|
||||
if test.wantBody != nil {
|
||||
require.NoError(t, err)
|
||||
@@ -430,7 +437,7 @@ func TestWebhook(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createSecretInformer(ctx context.Context, t *testing.T, kubeClient kubernetes.Interface) corev1informers.SecretInformer {
|
||||
func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1informers.SecretInformer {
|
||||
t.Helper()
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
|
||||
@@ -441,6 +448,9 @@ func createSecretInformer(ctx context.Context, t *testing.T, kubeClient kubernet
|
||||
// informer factory before syncing it.
|
||||
secretInformer.Informer()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
defer cancel()
|
||||
|
||||
kubeInformers.Start(ctx.Done())
|
||||
|
||||
informerTypesSynced := kubeInformers.WaitForCacheSync(ctx.Done())
|
||||
@@ -452,43 +462,40 @@ func createSecretInformer(ctx context.Context, t *testing.T, kubeClient kubernet
|
||||
// newClientProvider returns a dynamiccert.Provider configured
|
||||
// with valid serving cert, the CA bundle that can be used to verify the serving
|
||||
// cert, and the server name that can be used to verify the TLS peer.
|
||||
func newCertProvider(t *testing.T) (dynamiccert.Private, []byte, string) {
|
||||
func newCertProvider(t *testing.T) (dynamiccert.Provider, []byte, string) {
|
||||
t.Helper()
|
||||
|
||||
serverName := "local-user-authenticator"
|
||||
|
||||
ca, err := certauthority.New(serverName+" CA", time.Hour*24)
|
||||
ca, err := certauthority.New(pkix.Name{CommonName: serverName + " CA"}, time.Hour*24)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := ca.IssueServerCert([]string{serverName}, nil, time.Hour*24)
|
||||
cert, err := ca.Issue(pkix.Name{CommonName: serverName}, []string{serverName}, nil, time.Hour*24)
|
||||
require.NoError(t, err)
|
||||
|
||||
certPEM, keyPEM, err := certauthority.ToPEM(cert)
|
||||
require.NoError(t, err)
|
||||
|
||||
certProvider := dynamiccert.NewServingCert(t.Name())
|
||||
err = certProvider.SetCertKeyContent(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
certProvider := dynamiccert.New()
|
||||
certProvider.Set(certPEM, keyPEM)
|
||||
|
||||
return certProvider, ca.Bundle(), serverName
|
||||
}
|
||||
|
||||
// newClient creates an http.Client that can be used to make an HTTPS call to a
|
||||
// service whose serving certs can be verified by the provided CA bundle.
|
||||
func newClient(t *testing.T, caBundle []byte, serverName string) *http.Client {
|
||||
t.Helper()
|
||||
|
||||
func newClient(caBundle []byte, serverName string) *http.Client {
|
||||
rootCAs := x509.NewCertPool()
|
||||
ok := rootCAs.AppendCertsFromPEM(caBundle)
|
||||
require.True(t, ok)
|
||||
|
||||
c := phttp.Secure(rootCAs)
|
||||
|
||||
tlsConfig, err := utilnet.TLSClientConfig(c.Transport)
|
||||
require.NoError(t, err)
|
||||
tlsConfig.ServerName = serverName
|
||||
|
||||
return c
|
||||
rootCAs.AppendCertsFromPEM(caBundle)
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
RootCAs: rootCAs,
|
||||
ServerName: serverName,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newTokenReviewBody creates an io.ReadCloser that contains a JSON-encodeed
|
||||
@@ -519,7 +526,7 @@ func newTokenReviewBodyWithGVK(token string, gvk *schema.GroupVersionKind) (io.R
|
||||
},
|
||||
}
|
||||
err := json.NewEncoder(buf).Encode(&tr)
|
||||
return io.NopCloser(buf), err
|
||||
return ioutil.NopCloser(buf), err
|
||||
}
|
||||
|
||||
func unauthenticatedResponseJSON() *authenticationv1beta1.TokenReview {
|
||||
@@ -534,7 +541,7 @@ func unauthenticatedResponseJSON() *authenticationv1beta1.TokenReview {
|
||||
}
|
||||
}
|
||||
|
||||
func authenticatedResponseJSON(user string, groups []string) *authenticationv1beta1.TokenReview {
|
||||
func authenticatedResponseJSON(user, uid string, groups []string) *authenticationv1beta1.TokenReview {
|
||||
return &authenticationv1beta1.TokenReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "TokenReview",
|
||||
@@ -545,17 +552,19 @@ func authenticatedResponseJSON(user string, groups []string) *authenticationv1be
|
||||
User: authenticationv1beta1.UserInfo{
|
||||
Username: user,
|
||||
Groups: groups,
|
||||
UID: uid,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func addSecretToFakeClientTracker(t *testing.T, kubeClient *kubernetesfake.Clientset, username, password, groups string) {
|
||||
func addSecretToFakeClientTracker(t *testing.T, kubeClient *kubernetesfake.Clientset, username, uid, password, groups string) {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
require.NoError(t, err)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(uid),
|
||||
Name: username,
|
||||
Namespace: namespace,
|
||||
},
|
||||
@@ -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,128 +0,0 @@
|
||||
// Copyright 2021 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=="
|
||||
}`,
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
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 ...interface{}) {
|
||||
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-----
|
||||
35
cmd/pinniped-concierge/main.go
Normal file
35
cmd/pinniped-concierge/main.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/client-go/pkg/version"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/component-base/logs"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"go.pinniped.dev/internal/concierge/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logs.InitLogs()
|
||||
defer logs.FlushLogs()
|
||||
|
||||
// Dump out the time since compile (mostly useful for benchmarking our local development cycle latency).
|
||||
var timeSinceCompile time.Duration
|
||||
if buildDate, err := time.Parse(time.RFC3339, version.Get().BuildDate); err == nil {
|
||||
timeSinceCompile = time.Since(buildDate).Round(time.Second)
|
||||
}
|
||||
klog.Infof("Running %s at %#v (%s since build)", rest.DefaultKubernetesUserAgent(), version.Get(), timeSinceCompile)
|
||||
|
||||
ctx := genericapiserver.SetupSignalContext()
|
||||
|
||||
if err := server.New(ctx, os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,43 +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"
|
||||
|
||||
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"
|
||||
"go.pinniped.dev/internal/psets"
|
||||
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", psets.StringKeySet(subcommands).List(), binary))
|
||||
}
|
||||
subcommands[binary]()
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright 2021 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"},
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
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 ...interface{}) {
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
384
cmd/pinniped-supervisor/main.go
Normal file
384
cmd/pinniped-supervisor/main.go
Normal file
@@ -0,0 +1,384 @@
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/pkg/version"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/component-base/logs"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/klog/v2/klogr"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/config/v1alpha1"
|
||||
pinnipedclientset "go.pinniped.dev/generated/1.20/client/supervisor/clientset/versioned"
|
||||
pinnipedinformers "go.pinniped.dev/generated/1.20/client/supervisor/informers/externalversions"
|
||||
"go.pinniped.dev/internal/config/supervisor"
|
||||
"go.pinniped.dev/internal/controller/supervisorconfig"
|
||||
"go.pinniped.dev/internal/controller/supervisorconfig/generator"
|
||||
"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher"
|
||||
"go.pinniped.dev/internal/controller/supervisorstorage"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/deploymentref"
|
||||
"go.pinniped.dev/internal/downward"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
"go.pinniped.dev/internal/oidc/jwks"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/oidc/provider/manager"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/secret"
|
||||
)
|
||||
|
||||
const (
|
||||
singletonWorker = 1
|
||||
defaultResyncInterval = 3 * time.Minute
|
||||
)
|
||||
|
||||
func start(ctx context.Context, l net.Listener, handler http.Handler) {
|
||||
server := http.Server{Handler: handler}
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
errCh <- server.Serve(l)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
plog.Debug("server exited", "err", err)
|
||||
case <-ctx.Done():
|
||||
plog.Debug("server context cancelled", "err", ctx.Err())
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
plog.Debug("server shutdown failed", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func waitForSignal() os.Signal {
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
signal.Notify(signalCh, os.Interrupt)
|
||||
return <-signalCh
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func startControllers(
|
||||
ctx context.Context,
|
||||
cfg *supervisor.Config,
|
||||
issuerManager *manager.Manager,
|
||||
dynamicJWKSProvider jwks.DynamicJWKSProvider,
|
||||
dynamicTLSCertProvider provider.DynamicTLSCertProvider,
|
||||
dynamicUpstreamIDPProvider provider.DynamicUpstreamIDPProvider,
|
||||
secretCache *secret.Cache,
|
||||
supervisorDeployment *appsv1.Deployment,
|
||||
kubeClient kubernetes.Interface,
|
||||
pinnipedClient pinnipedclientset.Interface,
|
||||
kubeInformers kubeinformers.SharedInformerFactory,
|
||||
pinnipedInformers pinnipedinformers.SharedInformerFactory,
|
||||
) {
|
||||
federationDomainInformer := pinnipedInformers.Config().V1alpha1().FederationDomains()
|
||||
secretInformer := kubeInformers.Core().V1().Secrets()
|
||||
|
||||
// Create controller manager.
|
||||
controllerManager := controllerlib.
|
||||
NewManager().
|
||||
WithController(
|
||||
supervisorstorage.GarbageCollectorController(
|
||||
clock.RealClock{},
|
||||
kubeClient,
|
||||
secretInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewFederationDomainWatcherController(
|
||||
issuerManager,
|
||||
clock.RealClock{},
|
||||
pinnipedClient,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewJWKSWriterController(
|
||||
cfg.Labels,
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewJWKSObserverController(
|
||||
dynamicJWKSProvider,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewTLSCertObserverController(
|
||||
dynamicTLSCertProvider,
|
||||
cfg.NamesConfig.DefaultTLSCertificateSecret,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
generator.NewSupervisorSecretsController(
|
||||
supervisorDeployment,
|
||||
cfg.Labels,
|
||||
kubeClient,
|
||||
secretInformer,
|
||||
func(secret []byte) {
|
||||
plog.Debug("setting csrf cookie secret")
|
||||
secretCache.SetCSRFCookieEncoderHashKey(secret)
|
||||
},
|
||||
controllerlib.WithInformer,
|
||||
controllerlib.WithInitialEvent,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
generator.NewFederationDomainSecretsController(
|
||||
generator.NewSymmetricSecretHelper(
|
||||
"pinniped-oidc-provider-hmac-key-",
|
||||
cfg.Labels,
|
||||
rand.Reader,
|
||||
generator.SecretUsageTokenSigningKey,
|
||||
func(federationDomainIssuer string, symmetricKey []byte) {
|
||||
plog.Debug("setting hmac secret", "issuer", federationDomainIssuer)
|
||||
secretCache.SetTokenHMACKey(federationDomainIssuer, symmetricKey)
|
||||
},
|
||||
),
|
||||
func(fd *configv1alpha1.FederationDomain) *corev1.LocalObjectReference {
|
||||
return &fd.Status.Secrets.TokenSigningKey
|
||||
},
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
generator.NewFederationDomainSecretsController(
|
||||
generator.NewSymmetricSecretHelper(
|
||||
"pinniped-oidc-provider-upstream-state-signature-key-",
|
||||
cfg.Labels,
|
||||
rand.Reader,
|
||||
generator.SecretUsageStateSigningKey,
|
||||
func(federationDomainIssuer string, symmetricKey []byte) {
|
||||
plog.Debug("setting state signature key", "issuer", federationDomainIssuer)
|
||||
secretCache.SetStateEncoderHashKey(federationDomainIssuer, symmetricKey)
|
||||
},
|
||||
),
|
||||
func(fd *configv1alpha1.FederationDomain) *corev1.LocalObjectReference {
|
||||
return &fd.Status.Secrets.StateSigningKey
|
||||
},
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
generator.NewFederationDomainSecretsController(
|
||||
generator.NewSymmetricSecretHelper(
|
||||
"pinniped-oidc-provider-upstream-state-encryption-key-",
|
||||
cfg.Labels,
|
||||
rand.Reader,
|
||||
generator.SecretUsageStateEncryptionKey,
|
||||
func(federationDomainIssuer string, symmetricKey []byte) {
|
||||
plog.Debug("setting state encryption key", "issuer", federationDomainIssuer)
|
||||
secretCache.SetStateEncoderBlockKey(federationDomainIssuer, symmetricKey)
|
||||
},
|
||||
),
|
||||
func(fd *configv1alpha1.FederationDomain) *corev1.LocalObjectReference {
|
||||
return &fd.Status.Secrets.StateEncryptionKey
|
||||
},
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
upstreamwatcher.New(
|
||||
dynamicUpstreamIDPProvider,
|
||||
pinnipedClient,
|
||||
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
||||
secretInformer,
|
||||
klogr.New(),
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker)
|
||||
|
||||
kubeInformers.Start(ctx.Done())
|
||||
pinnipedInformers.Start(ctx.Done())
|
||||
|
||||
// Wait until the caches are synced before returning.
|
||||
kubeInformers.WaitForCacheSync(ctx.Done())
|
||||
pinnipedInformers.WaitForCacheSync(ctx.Done())
|
||||
|
||||
go controllerManager.Start(ctx)
|
||||
}
|
||||
|
||||
func run(podInfo *downward.PodInfo, cfg *supervisor.Config) error {
|
||||
serverInstallationNamespace := podInfo.Namespace
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// TODO remove code that relies on supervisorDeployment directly
|
||||
dref, supervisorDeployment, err := deploymentref.New(podInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create deployment ref: %w", err)
|
||||
}
|
||||
|
||||
client, err := kubeclient.New(dref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create k8s client: %w", err)
|
||||
}
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactoryWithOptions(
|
||||
client.Kubernetes,
|
||||
defaultResyncInterval,
|
||||
kubeinformers.WithNamespace(serverInstallationNamespace),
|
||||
)
|
||||
|
||||
pinnipedInformers := pinnipedinformers.NewSharedInformerFactoryWithOptions(
|
||||
client.PinnipedSupervisor,
|
||||
defaultResyncInterval,
|
||||
pinnipedinformers.WithNamespace(serverInstallationNamespace),
|
||||
)
|
||||
|
||||
// Serve the /healthz endpoint and make all other paths result in 404.
|
||||
healthMux := http.NewServeMux()
|
||||
healthMux.Handle("/healthz", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
_, _ = writer.Write([]byte("ok"))
|
||||
}))
|
||||
|
||||
dynamicJWKSProvider := jwks.NewDynamicJWKSProvider()
|
||||
dynamicTLSCertProvider := provider.NewDynamicTLSCertProvider()
|
||||
dynamicUpstreamIDPProvider := provider.NewDynamicUpstreamIDPProvider()
|
||||
secretCache := secret.Cache{}
|
||||
|
||||
// OIDC endpoints will be served by the oidProvidersManager, and any non-OIDC paths will fallback to the healthMux.
|
||||
oidProvidersManager := manager.NewManager(
|
||||
healthMux,
|
||||
dynamicJWKSProvider,
|
||||
dynamicUpstreamIDPProvider,
|
||||
&secretCache,
|
||||
client.Kubernetes.CoreV1().Secrets(serverInstallationNamespace),
|
||||
)
|
||||
|
||||
startControllers(
|
||||
ctx,
|
||||
cfg,
|
||||
oidProvidersManager,
|
||||
dynamicJWKSProvider,
|
||||
dynamicTLSCertProvider,
|
||||
dynamicUpstreamIDPProvider,
|
||||
&secretCache,
|
||||
supervisorDeployment,
|
||||
client.Kubernetes,
|
||||
client.PinnipedSupervisor,
|
||||
kubeInformers,
|
||||
pinnipedInformers,
|
||||
)
|
||||
|
||||
//nolint: gosec // Intentionally binding to all network interfaces.
|
||||
httpListener, err := net.Listen("tcp", ":8080")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create listener: %w", err)
|
||||
}
|
||||
defer func() { _ = httpListener.Close() }()
|
||||
start(ctx, httpListener, oidProvidersManager)
|
||||
|
||||
//nolint: gosec // Intentionally binding to all network interfaces.
|
||||
httpsListener, err := tls.Listen("tcp", ":8443", &tls.Config{
|
||||
MinVersion: tls.VersionTLS12, // Allow v1.2 because clients like the default `curl` on MacOS don't support 1.3 yet.
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert := dynamicTLSCertProvider.GetTLSCert(strings.ToLower(info.ServerName))
|
||||
defaultCert := dynamicTLSCertProvider.GetDefaultTLSCert()
|
||||
plog.Debug("GetCertificate called for port 8443",
|
||||
"info.ServerName", info.ServerName,
|
||||
"foundSNICert", cert != nil,
|
||||
"foundDefaultCert", defaultCert != nil,
|
||||
)
|
||||
if cert == nil {
|
||||
cert = defaultCert
|
||||
}
|
||||
return cert, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create listener: %w", err)
|
||||
}
|
||||
defer func() { _ = httpsListener.Close() }()
|
||||
start(ctx, httpsListener, oidProvidersManager)
|
||||
|
||||
plog.Debug("supervisor is ready",
|
||||
"httpAddress", httpListener.Addr().String(),
|
||||
"httpsAddress", httpsListener.Addr().String(),
|
||||
)
|
||||
|
||||
gotSignal := waitForSignal()
|
||||
plog.Debug("supervisor exiting", "signal", gotSignal)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
logs.InitLogs()
|
||||
defer logs.FlushLogs()
|
||||
plog.RemoveKlogGlobalFlags() // move this whenever the below code gets refactored to use cobra
|
||||
|
||||
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get())
|
||||
klog.Infof("Command-line arguments were: %s %s %s", os.Args[0], os.Args[1], os.Args[2])
|
||||
|
||||
// Discover in which namespace we are installed.
|
||||
podInfo, err := downward.Load(os.Args[1])
|
||||
if err != nil {
|
||||
klog.Fatal(fmt.Errorf("could not read pod metadata: %w", err))
|
||||
}
|
||||
|
||||
// Read the server config file.
|
||||
cfg, err := supervisor.FromPath(os.Args[2])
|
||||
if err != nil {
|
||||
klog.Fatal(fmt.Errorf("could not load config: %w", err))
|
||||
}
|
||||
|
||||
if err := run(podInfo, cfg); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
//nolint: gochecknoglobals
|
||||
var alphaCmd = &cobra.Command{
|
||||
Use: "alpha",
|
||||
Short: "alpha",
|
||||
@@ -16,7 +16,7 @@ var alphaCmd = &cobra.Command{
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(alphaCmd)
|
||||
}
|
||||
|
||||
@@ -22,9 +22,3 @@ func mustMarkHidden(cmd *cobra.Command, flags ...string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarkDeprecated(cmd *cobra.Command, flag, usageMessage string) {
|
||||
if err := cmd.Flags().MarkDeprecated(flag, usageMessage); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
136
cmd/pinniped/cmd/deprecated.go
Normal file
136
cmd/pinniped/cmd/deprecated.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(legacyGetKubeconfigCommand(kubeconfigRealDeps()))
|
||||
rootCmd.AddCommand(legacyExchangeTokenCommand(staticLoginRealDeps()))
|
||||
}
|
||||
|
||||
func legacyGetKubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = &cobra.Command{
|
||||
Hidden: true,
|
||||
Deprecated: "Please use `pinniped get kubeconfig` instead.",
|
||||
|
||||
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
||||
Use: "get-kubeconfig",
|
||||
Short: "Print a kubeconfig for authenticating into a cluster via Pinniped",
|
||||
Long: here.Doc(`
|
||||
Print a kubeconfig for authenticating into a cluster via Pinniped.
|
||||
Requires admin-like access to the cluster using the current
|
||||
kubeconfig context in order to access Pinniped's metadata.
|
||||
The current kubeconfig is found similar to how kubectl finds it:
|
||||
using the value of the --kubeconfig option, or if that is not
|
||||
specified then from the value of the KUBECONFIG environment
|
||||
variable, or if that is not specified then it defaults to
|
||||
.kube/config in your home directory.
|
||||
Prints a kubeconfig which is suitable to access the cluster using
|
||||
Pinniped as the authentication mechanism. This kubeconfig output
|
||||
can be saved to a file and used with future kubectl commands, e.g.:
|
||||
pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig
|
||||
kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods
|
||||
`),
|
||||
}
|
||||
token string
|
||||
kubeconfig string
|
||||
contextOverride string
|
||||
namespace string
|
||||
authenticatorType string
|
||||
authenticatorName string
|
||||
)
|
||||
|
||||
cmd.Flags().StringVar(&token, "token", "", "Credential to include in the resulting kubeconfig output (Required)")
|
||||
cmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
|
||||
cmd.Flags().StringVar(&contextOverride, "kubeconfig-context", "", "Kubeconfig context override")
|
||||
cmd.Flags().StringVar(&namespace, "pinniped-namespace", "pinniped-concierge", "Namespace in which Pinniped was installed")
|
||||
cmd.Flags().StringVar(&authenticatorType, "authenticator-type", "", "Authenticator type (e.g., 'webhook', 'jwt')")
|
||||
cmd.Flags().StringVar(&authenticatorName, "authenticator-name", "", "Authenticator name")
|
||||
mustMarkRequired(cmd, "token")
|
||||
plog.RemoveKlogGlobalFlags()
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return runGetKubeconfig(cmd.OutOrStdout(), deps, getKubeconfigParams{
|
||||
kubeconfigPath: kubeconfig,
|
||||
kubeconfigContextOverride: contextOverride,
|
||||
staticToken: token,
|
||||
concierge: getKubeconfigConciergeParams{
|
||||
namespace: namespace,
|
||||
authenticatorName: authenticatorName,
|
||||
authenticatorType: authenticatorType,
|
||||
},
|
||||
})
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func legacyExchangeTokenCommand(deps staticLoginDeps) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Hidden: true,
|
||||
Deprecated: "Please use `pinniped login static` instead.",
|
||||
|
||||
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
||||
Use: "exchange-credential",
|
||||
Short: "Exchange a credential for a cluster-specific access credential",
|
||||
Long: here.Doc(`
|
||||
Exchange a credential which proves your identity for a time-limited,
|
||||
cluster-specific access credential.
|
||||
Designed to be conveniently used as an credential plugin for kubectl.
|
||||
See the help message for 'pinniped get-kubeconfig' for more
|
||||
information about setting up a kubeconfig file using Pinniped.
|
||||
Requires all of the following environment variables, which are
|
||||
typically set in the kubeconfig:
|
||||
- PINNIPED_TOKEN: the token to send to Pinniped for exchange
|
||||
- PINNIPED_NAMESPACE: the namespace of the authenticator to authenticate
|
||||
against
|
||||
- PINNIPED_AUTHENTICATOR_TYPE: the type of authenticator to authenticate
|
||||
against (e.g., "webhook", "jwt")
|
||||
- PINNIPED_AUTHENTICATOR_NAME: the name of the authenticator to authenticate
|
||||
against
|
||||
- PINNIPED_CA_BUNDLE: the CA bundle to trust when calling
|
||||
Pinniped's HTTPS endpoint
|
||||
- PINNIPED_K8S_API_ENDPOINT: the URL for the Pinniped credential
|
||||
exchange API
|
||||
For more information about credential plugins in general, see
|
||||
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
||||
`),
|
||||
}
|
||||
plog.RemoveKlogGlobalFlags()
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
// Make a little helper to grab OS environment variables and keep a list that were missing.
|
||||
var missing []string
|
||||
getEnv := func(name string) string {
|
||||
value, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
return value
|
||||
}
|
||||
flags := staticLoginParams{
|
||||
staticToken: getEnv("PINNIPED_TOKEN"),
|
||||
conciergeEnabled: true,
|
||||
conciergeNamespace: getEnv("PINNIPED_NAMESPACE"),
|
||||
conciergeAuthenticatorType: getEnv("PINNIPED_AUTHENTICATOR_TYPE"),
|
||||
conciergeAuthenticatorName: getEnv("PINNIPED_AUTHENTICATOR_NAME"),
|
||||
conciergeEndpoint: getEnv("PINNIPED_K8S_API_ENDPOINT"),
|
||||
conciergeCABundle: base64.StdEncoding.EncodeToString([]byte(getEnv("PINNIPED_CA_BUNDLE"))),
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("failed to get credential: required environment variable(s) not set: %v", missing)
|
||||
}
|
||||
return runStaticLogin(cmd.OutOrStdout(), deps, flags)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// Copyright 2021-2022 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"
|
||||
|
||||
configv1alpha1 "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 *configv1alpha1.CredentialIssuerFrontend) bool {
|
||||
switch *f {
|
||||
case modeImpersonationProxy:
|
||||
return frontend.Type == configv1alpha1.ImpersonationProxyFrontendType
|
||||
case modeTokenCredentialRequestAPI:
|
||||
return frontend.Type == configv1alpha1.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,73 +0,0 @@
|
||||
// Copyright 2021-2022 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"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
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(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType}))
|
||||
|
||||
require.NoError(t, f.Set("TokenCredentialRequestAPI"))
|
||||
require.Equal(t, modeTokenCredentialRequestAPI, f)
|
||||
require.Equal(t, "TokenCredentialRequestAPI", f.String())
|
||||
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.False(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.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(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.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 := testutil.TempDir(t)
|
||||
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-2022 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,
|
||||
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,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
//nolint: gochecknoglobals
|
||||
var getCmd = &cobra.Command{Use: "get", Short: "get"}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(getCmd)
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
)
|
||||
|
||||
// getConciergeClientsetFunc is a function that can return a clientset for the Concierge API given a
|
||||
// clientConfig and the apiGroupSuffix with which the API is running.
|
||||
type getConciergeClientsetFunc func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error)
|
||||
|
||||
// getRealConciergeClientset returns a real implementation of a conciergeclientset.Interface.
|
||||
func getRealConciergeClientset(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) {
|
||||
restConfig, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := kubeclient.New(
|
||||
kubeclient.WithConfig(restConfig),
|
||||
kubeclient.WithMiddleware(groupsuffix.New(apiGroupSuffix)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PinnipedConcierge, 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,15 +1,13 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 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"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
//nolint: gochecknoglobals
|
||||
var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "login",
|
||||
@@ -18,19 +16,7 @@ var loginCmd = &cobra.Command{
|
||||
Hidden: true, // These commands are not really meant to be used directly by users, so it's confusing to have them discoverable.
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
//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,60 +1,46 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
"k8s.io/klog/v2/klogr"
|
||||
|
||||
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/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"
|
||||
)
|
||||
|
||||
//nolint:gochecknoinits
|
||||
//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)
|
||||
}
|
||||
|
||||
func oidcLoginCommandRealDeps() oidcLoginCommandDeps {
|
||||
return oidcLoginCommandDeps{
|
||||
lookupEnv: os.LookupEnv,
|
||||
login: oidcclient.Login,
|
||||
login: oidcclient.Login,
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
return client.ExchangeToken(ctx, token)
|
||||
},
|
||||
@@ -62,89 +48,66 @@ func oidcLoginCommandRealDeps() oidcLoginCommandDeps {
|
||||
}
|
||||
|
||||
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
|
||||
issuer string
|
||||
clientID string
|
||||
listenPort uint16
|
||||
scopes []string
|
||||
skipBrowser bool
|
||||
sessionCachePath string
|
||||
caBundlePaths []string
|
||||
caBundleData []string
|
||||
debugSessionCache bool
|
||||
requestAudience string
|
||||
conciergeEnabled bool
|
||||
conciergeNamespace string
|
||||
conciergeAuthenticatorType string
|
||||
conciergeAuthenticatorName string
|
||||
conciergeEndpoint string
|
||||
conciergeCABundle string
|
||||
}
|
||||
|
||||
func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = &cobra.Command{
|
||||
cmd = cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "oidc --issuer ISSUER",
|
||||
Short: "Login using an OpenID Connect provider",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags oidcLoginFlags
|
||||
conciergeNamespace string // unused now
|
||||
flags oidcLoginFlags
|
||||
)
|
||||
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().StringVar(&flags.clientID, "client-id", "pinniped-cli", "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().StringSliceVar(&flags.scopes, "scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "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().StringSliceVar(&flags.caBundleData, "ca-bundle-data", nil, "Base64 endcoded 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().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Exchange the OIDC ID token with the Pinniped concierge during login")
|
||||
cmd.Flags().StringVar(&flags.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')", idpdiscoveryv1alpha1.IDPTypeOIDC, idpdiscoveryv1alpha1.IDPTypeLDAP, idpdiscoveryv1alpha1.IDPTypeActiveDirectory))
|
||||
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))
|
||||
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
||||
|
||||
// --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")
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error {
|
||||
// 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 the hidden --debug-session-cache option is passed, log all the errors from the session cache with klog.
|
||||
if flags.debugSessionCache {
|
||||
logger := plog.WithName("session")
|
||||
logger := klogr.New().WithName("session")
|
||||
sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) {
|
||||
logger.Error("error during session cache operation", err)
|
||||
logger.Error(err, "error during session cache operation")
|
||||
}))
|
||||
}
|
||||
sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...)
|
||||
@@ -152,7 +115,6 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
// Initialize the login handler.
|
||||
opts := []oidcclient.Option{
|
||||
oidcclient.WithContext(cmd.Context()),
|
||||
oidcclient.WithLogger(plog.Logr()), //nolint:staticcheck // old code with lots of log statements
|
||||
oidcclient.WithScopes(flags.scopes),
|
||||
oidcclient.WithSessionCache(sessionCache),
|
||||
}
|
||||
@@ -165,43 +127,26 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
opts = append(opts, oidcclient.WithRequestAudience(flags.requestAudience))
|
||||
}
|
||||
|
||||
if flags.upstreamIdentityProviderName != "" {
|
||||
opts = append(opts, oidcclient.WithUpstreamIdentityProvider(
|
||||
flags.upstreamIdentityProviderName, flags.upstreamIdentityProviderType))
|
||||
}
|
||||
|
||||
flowOpts, err := flowOptions(
|
||||
idpdiscoveryv1alpha1.IDPType(flags.upstreamIdentityProviderType),
|
||||
idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow),
|
||||
deps,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts = append(opts, flowOpts...)
|
||||
|
||||
var concierge *conciergeclient.Client
|
||||
if flags.conciergeEnabled {
|
||||
var err error
|
||||
concierge, err = conciergeclient.New(
|
||||
conciergeclient.WithNamespace(flags.conciergeNamespace),
|
||||
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
|
||||
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
|
||||
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
|
||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Concierge parameters: %w", err)
|
||||
return fmt.Errorf("invalid concierge parameters: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --skip-browser skips opening the browser.
|
||||
// --skip-browser replaces the default "browser open" function with one that prints to stderr.
|
||||
if flags.skipBrowser {
|
||||
opts = append(opts, oidcclient.WithSkipBrowserOpen())
|
||||
}
|
||||
|
||||
// --skip-listen skips starting the localhost callback listener.
|
||||
if flags.skipListen {
|
||||
opts = append(opts, oidcclient.WithSkipListen())
|
||||
opts = append(opts, oidcclient.WithBrowserOpen(func(url string) error {
|
||||
cmd.PrintErr("Please log in: ", url, "\n")
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
if len(flags.caBundlePaths) > 0 || len(flags.caBundleData) > 0 {
|
||||
@@ -211,24 +156,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
}
|
||||
opts = append(opts, oidcclient.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.
|
||||
token, err := deps.login(flags.issuer, flags.clientID, opts...)
|
||||
if err != nil {
|
||||
@@ -237,86 +165,21 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
cred := tokenCredential(token)
|
||||
|
||||
// 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()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if concierge != nil {
|
||||
cred, err = deps.exchangeToken(ctx, concierge, token.IDToken.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
|
||||
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 flowOptions(
|
||||
requestedIDPType idpdiscoveryv1alpha1.IDPType,
|
||||
requestedFlow idpdiscoveryv1alpha1.IDPFlow,
|
||||
deps oidcLoginCommandDeps,
|
||||
) ([]oidcclient.Option, error) {
|
||||
useCLIFlow := []oidcclient.Option{oidcclient.WithCLISendingCredentials()}
|
||||
|
||||
// If the env var is set to override the --upstream-identity-provider-type flag, then override it.
|
||||
flowOverride, hasFlowOverride := deps.lookupEnv(upstreamIdentityProviderFlowEnvVarName)
|
||||
flowSource := "--upstream-identity-provider-flow"
|
||||
if hasFlowOverride {
|
||||
requestedFlow = idpdiscoveryv1alpha1.IDPFlow(flowOverride)
|
||||
flowSource = upstreamIdentityProviderFlowEnvVarName
|
||||
}
|
||||
|
||||
switch requestedIDPType {
|
||||
case idpdiscoveryv1alpha1.IDPTypeOIDC:
|
||||
switch requestedFlow {
|
||||
case idpdiscoveryv1alpha1.IDPFlowCLIPassword:
|
||||
return useCLIFlow, nil
|
||||
case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode, "":
|
||||
return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"%s value not recognized for identity provider type %q: %s (supported values: %s)",
|
||||
flowSource, requestedIDPType, requestedFlow,
|
||||
strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String(), idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()}, ", "))
|
||||
}
|
||||
case idpdiscoveryv1alpha1.IDPTypeLDAP, idpdiscoveryv1alpha1.IDPTypeActiveDirectory:
|
||||
switch requestedFlow {
|
||||
case idpdiscoveryv1alpha1.IDPFlowCLIPassword, "":
|
||||
return useCLIFlow, nil
|
||||
case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode:
|
||||
return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"%s value not recognized for identity provider type %q: %s (supported values: %s)",
|
||||
flowSource, requestedIDPType, requestedFlow,
|
||||
strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String(), idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String()}, ", "))
|
||||
}
|
||||
default:
|
||||
// Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236
|
||||
return nil, fmt.Errorf(
|
||||
"--upstream-identity-provider-type value not recognized: %s (supported values: %s)",
|
||||
requestedIDPType,
|
||||
strings.Join([]string{
|
||||
idpdiscoveryv1alpha1.IDPTypeOIDC.String(),
|
||||
idpdiscoveryv1alpha1.IDPTypeLDAP.String(),
|
||||
idpdiscoveryv1alpha1.IDPTypeActiveDirectory.String(),
|
||||
}, ", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
|
||||
pool := x509.NewCertPool()
|
||||
for _, p := range caBundlePaths {
|
||||
pem, err := os.ReadFile(p)
|
||||
pem, err := ioutil.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read --ca-bundle: %w", err)
|
||||
}
|
||||
@@ -329,7 +192,15 @@ func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, er
|
||||
}
|
||||
pool.AppendCertsFromPEM(pem)
|
||||
}
|
||||
return phttp.Default(pool), nil
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: pool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
|
||||
@@ -348,26 +219,10 @@ func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
|
||||
return &cred
|
||||
}
|
||||
|
||||
func SetLogLevel(ctx context.Context, lookupEnv func(string) (string, bool)) (plog.Logger, error) {
|
||||
debug, _ := lookupEnv("PINNIPED_DEBUG")
|
||||
if debug == "true" {
|
||||
err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug, Format: plog.FormatCLI})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
logger := plog.New().WithName("pinniped-login")
|
||||
return logger, 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
|
||||
*/
|
||||
// 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"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
@@ -6,23 +6,20 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
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"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
@@ -32,29 +29,23 @@ import (
|
||||
func TestLoginOIDCCommand(t *testing.T) {
|
||||
cfgDir := mustGetConfigDir()
|
||||
|
||||
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||
testCA, err := certauthority.New(pkix.Name{CommonName: "Test CA"}, 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
tmpdir := testutil.TempDir(t)
|
||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||
require.NoError(t, os.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
|
||||
require.NoError(t, ioutil.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)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
loginErr error
|
||||
conciergeErr error
|
||||
env map[string]string
|
||||
wantError bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantOptionsCount int
|
||||
wantLogs []string
|
||||
}{
|
||||
{
|
||||
name: "help flag passed",
|
||||
@@ -66,26 +57,22 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
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') (default "oidc")
|
||||
--ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
||||
--ca-bundle-data strings Base64 endcoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)
|
||||
--client-id string OpenID Connect client ID (default "pinniped-cli")
|
||||
--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 Pinniped concierge endpoint
|
||||
--concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge")
|
||||
--enable-concierge Exchange the OIDC ID token with the Pinniped concierge during 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])
|
||||
--session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml")
|
||||
--skip-browser Skip opening the browser (just print the URL)
|
||||
`),
|
||||
},
|
||||
{
|
||||
@@ -105,7 +92,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: endpoint must not be empty
|
||||
Error: invalid concierge parameters: endpoint must not be empty
|
||||
`),
|
||||
},
|
||||
{
|
||||
@@ -132,323 +119,14 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
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/",
|
||||
},
|
||||
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: "invalid upstream type is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--upstream-identity-provider-type", "invalid",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid upstream type when flow override env var is used is still an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--upstream-identity-provider-type", "invalid",
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory)
|
||||
`),
|
||||
},
|
||||
{
|
||||
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
|
||||
},
|
||||
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: "oidc upstream type with CLI flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
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: "oidc upstream type with browser flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
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: "oidc upstream type with CLI flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--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": "cli_password"},
|
||||
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: "oidc upstream type with with browser flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--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": "browser_authcode"},
|
||||
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: "oidc upstream type with unsupported flow is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "foobar",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-flow value not recognized for identity provider type "oidc": foobar (supported values: browser_authcode, cli_password)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with unsupported flow in flow override env var is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--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": "foo"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "oidc": foo (supported values: browser_authcode, cli_password)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with default flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
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: "activedirectory upstream type with default flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
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: "ldap upstream type with CLI flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
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: "ldap upstream type with browser_authcode flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
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: "ldap upstream type with CLI flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--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": "cli_password"},
|
||||
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: "ldap upstream type with browser_authcode flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--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": "browser_authcode"},
|
||||
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: "ldap upstream type with unsupported flow is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "foo",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with unsupported flow in flow override env var is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--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": "foo"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "active directory upstream type with CLI flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
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: "active directory upstream type with browser_authcode is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
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: "active directory upstream type with CLI flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--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": "cli_password"},
|
||||
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: "active directory upstream type with browser_authcode in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--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": "browser_authcode"},
|
||||
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: "active directory upstream type with unsupported flow is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "foo",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "active directory upstream type with unsupported flow in flow override env var is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--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": "foo"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode)
|
||||
`),
|
||||
},
|
||||
{
|
||||
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"),
|
||||
wantOptionsCount: 4,
|
||||
wantOptionsCount: 3,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not complete Pinniped login: some login error
|
||||
@@ -463,13 +141,12 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--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"),
|
||||
wantOptionsCount: 4,
|
||||
wantOptionsCount: 3,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not complete Concierge credential exchange: some concierge error
|
||||
Error: could not complete concierge credential exchange: some concierge error
|
||||
`),
|
||||
},
|
||||
{
|
||||
@@ -477,15 +154,9 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
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"},
|
||||
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 + ` pinniped-login cmd/login_oidc.go:231 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:251 No concierge configured, skipping token credential exchange`,
|
||||
},
|
||||
wantOptionsCount: 3,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "success with all options",
|
||||
@@ -493,48 +164,29 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--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-namespace", "test-namespace",
|
||||
"--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", testutil.TempDir(t) + "/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",
|
||||
},
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true"},
|
||||
wantOptionsCount: 11,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n",
|
||||
wantLogs: []string{
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:231 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:241 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:249 Successfully exchanged token for cluster credential.`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:256 caching cluster credential for future use.`,
|
||||
},
|
||||
wantOptionsCount: 7,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"exchanged-token"}}` + "\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
fakeClock := clocktesting.NewFakeClock(now)
|
||||
ctx := plog.TestZapOverrides(context.Background(), t, &buf, nil, zap.WithClock(plog.ZapClock(fakeClock)))
|
||||
|
||||
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)
|
||||
@@ -571,7 +223,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs(tt.args)
|
||||
err = cmd.ExecuteContext(ctx)
|
||||
err := cmd.Execute()
|
||||
if tt.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
@@ -580,16 +232,6 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
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, logLines(buf.String()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func logLines(logs string) []string {
|
||||
if len(logs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(strings.TrimSpace(logs), "\n")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
@@ -7,21 +7,18 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"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/plog"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
|
||||
//nolint:gochecknoinits
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps()))
|
||||
}
|
||||
@@ -44,51 +41,36 @@ type staticLoginParams struct {
|
||||
staticToken string
|
||||
staticTokenEnvName string
|
||||
conciergeEnabled bool
|
||||
conciergeNamespace string
|
||||
conciergeAuthenticatorType string
|
||||
conciergeAuthenticatorName string
|
||||
conciergeEndpoint string
|
||||
conciergeCABundle string
|
||||
conciergeAPIGroupSuffix string
|
||||
credentialCachePath string
|
||||
}
|
||||
|
||||
func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = &cobra.Command{
|
||||
cmd = cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "static [--token TOKEN] [--token-env TOKEN_NAME]",
|
||||
Short: "Login using a static token",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags staticLoginParams
|
||||
conciergeNamespace string // unused now
|
||||
flags staticLoginParams
|
||||
)
|
||||
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().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Exchange the token with the Pinniped concierge during login")
|
||||
cmd.Flags().StringVar(&flags.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
|
||||
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) }
|
||||
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)
|
||||
}
|
||||
|
||||
func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error {
|
||||
if flags.staticToken == "" && flags.staticTokenEnvName == "" {
|
||||
return fmt.Errorf("one of --token or --token-env must be set")
|
||||
}
|
||||
@@ -97,13 +79,13 @@ func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginP
|
||||
if flags.conciergeEnabled {
|
||||
var err error
|
||||
concierge, err = conciergeclient.New(
|
||||
conciergeclient.WithNamespace(flags.conciergeNamespace),
|
||||
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
|
||||
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
|
||||
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
|
||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Concierge parameters: %w", err)
|
||||
return fmt.Errorf("invalid concierge parameters: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,44 +105,16 @@ func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginP
|
||||
}
|
||||
cred := tokenCredential(&oidctypes.Token{IDToken: &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.
|
||||
// Exchange that token with the concierge, if configured.
|
||||
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)
|
||||
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,4 +1,4 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
@@ -6,37 +6,29 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
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)
|
||||
testCA, err := certauthority.New(pkix.Name{CommonName: "Test CA"}, 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
tmpdir := testutil.TempDir(t)
|
||||
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)
|
||||
require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -48,7 +40,6 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantOptionsCount int
|
||||
wantLogs []string
|
||||
}{
|
||||
{
|
||||
name: "help flag passed",
|
||||
@@ -60,13 +51,12 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
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
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the concierge
|
||||
--concierge-endpoint string API base for the Pinniped concierge endpoint
|
||||
--concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge")
|
||||
--enable-concierge Exchange the token with the Pinniped concierge during login
|
||||
-h, --help help for static
|
||||
--token string Static token to present during login
|
||||
--token-env string Environment variable containing a static token
|
||||
@@ -88,7 +78,7 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: endpoint must not be empty
|
||||
Error: invalid concierge parameters: endpoint must not be empty
|
||||
`),
|
||||
},
|
||||
{
|
||||
@@ -122,7 +112,7 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
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",
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"test-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "concierge failure",
|
||||
@@ -134,28 +124,9 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
"--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 + ` pinniped-login cmd/login_static.go:147 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])?)*')
|
||||
Error: could not complete concierge credential exchange: some concierge error
|
||||
`),
|
||||
},
|
||||
{
|
||||
@@ -163,17 +134,12 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
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",
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"test-token"}}` + "\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
fakeClock := clocktesting.NewFakeClock(now)
|
||||
ctx := plog.TestZapOverrides(context.Background(), t, &buf, nil, zap.WithClock(plog.ZapClock(fakeClock)))
|
||||
|
||||
cmd := staticLoginCommand(staticLoginDeps{
|
||||
lookupEnv: func(s string) (string, bool) {
|
||||
v, ok := tt.env[s]
|
||||
@@ -201,7 +167,7 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs(tt.args)
|
||||
err := cmd.ExecuteContext(ctx)
|
||||
err := cmd.Execute()
|
||||
if tt.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
@@ -209,8 +175,6 @@ func TestLoginStaticCommand(t *testing.T) {
|
||||
}
|
||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
||||
|
||||
require.Equal(t, tt.wantLogs, logLines(buf.String()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
//nolint: gochecknoglobals
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "pinniped",
|
||||
Short: "pinniped",
|
||||
@@ -21,11 +19,8 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
// 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
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
20
cmd/pinniped/cmd/testdata/kubeconfig.yaml
vendored
20
cmd/pinniped/cmd/testdata/kubeconfig.yaml
vendored
@@ -3,33 +3,25 @@ clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== # fake-certificate-authority-data-value
|
||||
server: https://fake-server-url-value
|
||||
name: kind-cluster
|
||||
name: kind-kind
|
||||
- cluster:
|
||||
certificate-authority-data: c29tZS1vdGhlci1mYWtlLWNlcnRpZmljYXRlLWF1dGhvcml0eS1kYXRhLXZhbHVl # some-other-fake-certificate-authority-data-value
|
||||
server: https://some-other-fake-server-url-value
|
||||
name: some-other-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-cluster
|
||||
user: kind-user
|
||||
name: kind-context
|
||||
cluster: kind-kind
|
||||
user: kind-kind
|
||||
name: kind-kind
|
||||
- context:
|
||||
cluster: some-other-cluster
|
||||
user: some-other-user
|
||||
name: some-other-context
|
||||
- context:
|
||||
cluster: invalid-cluster
|
||||
user: some-other-user
|
||||
name: invalid-context-no-such-cluster
|
||||
- context:
|
||||
cluster: some-other-cluster
|
||||
user: invalid-user
|
||||
name: invalid-context-no-such-user
|
||||
current-context: kind-context
|
||||
current-context: kind-kind
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kind-user
|
||||
- name: kind-kind
|
||||
user:
|
||||
client-certificate-data: ZmFrZS1jbGllbnQtY2VydGlmaWNhdGUtZGF0YS12YWx1ZQ== # fake-client-certificate-data-value
|
||||
client-key-data: ZmFrZS1jbGllbnQta2V5LWRhdGEtdmFsdWU= # fake-client-key-data-value
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user