Compare commits

..

1 Commits

Author SHA1 Message Date
Margo Crawford
8a4bbbfcbe Proof of concept: client-side logout
Signed-off-by: Margo Crawford <margaretc@vmware.com>
2022-03-04 17:01:26 -08:00
3252 changed files with 127038 additions and 432858 deletions

View File

@@ -21,6 +21,3 @@
# MacOS Desktop Services Store
.DS_Store
# Hugo temp file
.hugo_build.lock

1
.gitattributes vendored
View File

@@ -1,3 +1,2 @@
*.go.tmpl linguist-language=Go
hack/Dockerfile_fips linguist-language=Dockerfile
generated/** linguist-generated

View File

@@ -1,36 +0,0 @@
---
name: Add new K8s version
about: 'Checklist for maintainers to add new K8s minor version'
title: 'Add new K8s version vX.X'
labels: ''
assignees: ''
---
<!-- Note: Please update the issue title to include the new Kubernetes version number. -->
# Adding a new Kubernetes Version
## `pinniped's ci branch`
- [ ] Update `dockerfile-builders` pipeline
- [ ] Update `pull-requests` pipeline
- [ ] Update `main` pipeline
## `pinniped`
- [ ] Bump all golang dependencies (especially the `k8s.io` dependencies to use the new minor version).
- [ ] Be sure to verify that everything compiles and unit tests pass locally. This is probably a good starting point.
```shell
./hack/update-go-mod/update-go-mod.sh
./hack/module.sh unit
./hack/prepare-for-integration-tests.sh
```
- [ ] Log in to github as pinniped-ci-bot, then go to [this page](https://github.com/pinniped-ci-bot?tab=packages) and change the settings for the new `k8s-code-generator-1.*` image to be publicly visible
- [ ] Add the new K8s version to `hack/lib/kube-versions.txt` and run code generation.
## General Tasks
- [ ] Consider dropping support for any older versions of Kubernetes
- [ ] Create stories or chores to take advantage of features in the new Kubernetes version
- [ ] Close this issue

View File

@@ -1,31 +0,0 @@
---
name: Release checklist
about: Checklist for maintainers to prepare for an upcoming release
title: 'Release checklist for vX.X.X'
labels: ''
assignees: ''
---
<!-- Note: Please update the issue title to include the planned release's version number. -->
# Release checklist
- [ ] Ensure that Pinniped's dependencies have been upgraded, to the extent desired by the team (refer to the diff output from the latest run of the [all-golang-deps-updated](https://ci.pinniped.dev/teams/main/pipelines/security-scan/jobs/all-golang-deps-updated/) CI job)
- [ ] If you are updating golang in Pinniped, be sure to update golang in CI as well. Do a search-and-replace to update the version number everywhere in the pinniped `ci` branch.
- [ ] If the Fosite library is being updated and the format of the content of the Supervisor's storage Secrets are changed, or if any change to our own code changes the format of the content of the Supervisor's session storage Secrets, then be sure to update the `accessTokenStorageVersion`, `authorizeCodeStorageVersion`, `oidcStorageVersion`, `pkceStorageVersion`, `refreshTokenStorageVersion`, variables in files such as `internal/fositestorage/accesstoken/accesstoken.go`. Failing tests should signal the need to update these values.
- [ ] For go.mod direct dependencies that are v2 or above, such as `github.com/google/go-github/vXX`, check to see if there is a new major version available. Try using `hack/update-go-mod/update-majors.sh`.
- [ ] Evaluate all `replace` directives in the `go.mod` file. Are they up to date versions? Can any `replace` directives be removed?
- [ ] Ensure that Pinniped's codegen is up-to-date with the latest Kubernetes releases by making sure this [file](https://github.com/vmware-tanzu/pinniped/blob/main/hack/lib/kube-versions.txt) is updated compared to the latest releases listed [here for active branches](https://kubernetes.io/releases/) and [here for non-active branches](https://kubernetes.io/releases/patch-releases/#non-active-branch-history)
- [ ] Ensure that the `k8s-code-generator` CI job definitions are up-to-date with the latest Go, K8s, and `controller-gen` versions
- [ ] All relevant feature and docs PRs are merged
- [ ] The [main pipeline](https://ci.pinniped.dev/teams/main/pipelines/main) is green, up to and including the `ready-to-release` job. Check that the expected git commit has passed the `ready-to-release` job.
- [ ] Optional: a blog post for the release is written and submitted as a PR but not merged yet
- [ ] All merged user stories are accepted (manually tested)
- [ ] Only after all stories are accepted, manually trigger the `release` job to create a draft GitHub release
- [ ] Manually edit the draft release notes on the [GitHub release](https://github.com/vmware-tanzu/pinniped/releases) to describe the contents of the release, using the format which was automatically added to the draft release
- [ ] Publish (i.e. make public) the draft release
- [ ] After making the release public, the jobs in the [main pipeline](https://ci.pinniped.dev/teams/main/pipelines/main) beyond the release job should auto-trigger, so check to make sure that they passed
- [ ] Edit the blog post's date to make it match the actual release date, and merge the blog post PR to make it live on the website
- [ ] Publicize the release via tweets, etc.
- [ ] Close this issue

135
.github/dependabot.yml vendored
View File

@@ -2,144 +2,13 @@
version: 2
updates:
- package-ecosystem: "gomod"
open-pull-requests-limit: 2
directory: "/hack/update-go-mod"
schedule:
interval: "daily"
# Use dependabot to automate major-only dependency bumps
- package-ecosystem: "gomod"
open-pull-requests-limit: 2 # Not sure why there would ever be more than 1, just would not want to hide anything
open-pull-requests-limit: 100
directory: "/"
schedule:
interval: "daily"
# group all major dependency bumps together so there's only one pull request
groups:
go-modules:
patterns:
- "*"
update-types:
- "major"
ignore:
# For all packages, ignore all minor and patch updates
- dependency-name: "*"
update-types:
- "version-update:semver-minor"
- "version-update:semver-patch"
# Our own CI job is responsible for updating this Docker file now.
# - package-ecosystem: "docker"
# directory: "/"
# schedule:
# interval: "daily"
# Our own CI job is responsible for updating this Docker file now.
# - package-ecosystem: "docker"
# directory: "/hack" # this should keep the FIPS dockerfile updated per https://github.com/dependabot/feedback/issues/145#issuecomment-414738498
# schedule:
# interval: "daily"
- package-ecosystem: "docker"
directory: "/dockerfiles/code-coverage-uploader/"
open-pull-requests-limit: 100
directory: "/"
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/crane/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/deployment-yaml-formatter/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/eks-deployer/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/gh-cli/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/go-lint-runner/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/integration-test-runner/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/integration-test-runner-beta/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/k8s-app-deployer/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/k8s-code-generator/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/pool-trigger-resource/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/test-bitnami-ldap/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/test-cfssl/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/test-dex/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/test-forward-proxy/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/dockerfiles/test-kubectl/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci
- package-ecosystem: "docker"
directory: "/pipelines/shared-helpers/test-binaries-image/"
open-pull-requests-limit: 100
schedule:
interval: "daily"
target-branch: ci

View File

@@ -1,23 +1,18 @@
# See https://codeql.github.com and https://github.com/github/codeql-action
# This action runs GitHub's industry-leading semantic code analysis engine, CodeQL, against a
# repository's source code to find security vulnerabilities. It then automatically uploads the
# results to GitHub so they can be displayed in the repository's security tab.
name: "CodeQL"
on:
push:
branches: [ "main", release* ]
branches: [ main, release* ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
branches: [ main, release* ]
schedule:
- cron: '24 3 * * 3'
- cron: '39 13 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
@@ -29,44 +24,34 @@ jobs:
language: [ 'go', 'javascript' ]
steps:
# Checkout our repository.
# See https://github.com/actions/checkout for documentation.
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
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.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Install Go.
# See https://github.com/actions/setup-go?tab=readme-ov-file#getting-go-version-from-the-gomod-file.
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
# 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@v3
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.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# ✏️ 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: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
uses: github/codeql-action/analyze@v1

3
.gitignore vendored
View File

@@ -19,6 +19,3 @@
# MacOS Desktop Services Store
.DS_Store
# Hugo temp file
.hugo_build.lock

View File

@@ -1,69 +1,61 @@
# https://golangci-lint.run/usage/configuration/
# https://github.com/golangci/golangci-lint#config-file
run:
timeout: 1m
deadline: 1m
skip-dirs:
- generated
linters:
disable-all: true
enable:
# default linters
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
# 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
- bodyclose
# - depguard
- dogsled
- exhaustive
- funlen
- gochecknoglobals
- gochecknoinits
- gocritic
- gocyclo
- godot
- goheader
- goimports
- revive
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- noctx
- nolintlint
- prealloc
- rowserrcheck
- sqlclosecheck
- unconvert
- whitespace
- copyloopvar
- intrange
# - fatcontext Starting in go@1.23.1 and golangci-lint@1.61.0 this gave a lot of false positives
# - canonicalheader Can't do this one since it alerts on valid headers such as X-XSS-Protection
- spancheck
- importas
- makezero
- prealloc
- gofmt
# additional linters for this project (we should disable these if they get annoying).
- asciicheck
- bodyclose
- depguard
- dogsled
- exhaustive
- exportloopref
- funlen
- gochecknoglobals
- gochecknoinits
- gocritic
- gocyclo
- godot
- goheader
- goimports
- golint
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- noctx
- nolintlint
- prealloc
- rowserrcheck
- scopelint
- sqlclosecheck
- unconvert
- whitespace
issues:
exclude-dirs:
- generated
exclude-rules:
# exclude tests from some rules for things that are useful in a testing context.
- path: _test\.go
linters:
- funlen
- gochecknoglobals
- revive
- path: internal/testutil/
linters:
- revive
- path: _test\.go
linters:
- funlen
- gochecknoglobals
linters-settings:
funlen:
@@ -75,88 +67,7 @@ linters-settings:
# YYYY or YYYY-YYYY
YEARS: \d\d\d\d(-\d\d\d\d)?
template: |-
Copyright {{YEARS}} the Pinniped contributors. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
Copyright {{YEARS}} the Pinniped contributors. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
goimports:
local-prefixes: go.pinniped.dev
revive:
max-open-files: 2048
rules:
- name: unused-parameter
arguments:
# Allow unused params that start with underscore. It can be nice to keep unused param names when implementing
# an interface sometimes, to help readers understand why it is unused in that particular implementation.
- allowRegex: "^_"
spancheck:
# https://golangci-lint.run/usage/linters/#spancheck
checks:
- end
- record-error
- set-status
importas:
no-unaliased: true # All packages explicitly listed below must be aliased
no-extra-aliases: false # Allow other aliases than the ones explicitly listed below
alias:
# k8s.io/apimachinery
- pkg: k8s.io/apimachinery/pkg/util/errors
alias: utilerrors
- pkg: k8s.io/apimachinery/pkg/api/errors
alias: apierrors
- pkg: k8s.io/apimachinery/pkg/apis/meta/v1
alias: metav1
# k8s.io
- pkg: k8s.io/api/core/v1
alias: corev1
# OAuth2/OIDC/Fosite/JOSE
- pkg: github.com/coreos/go-oidc/v3/oidc
alias: coreosoidc
- pkg: github.com/ory/fosite/handler/oauth2
alias: fositeoauth2
- pkg: github.com/ory/fosite/token/jwt
alias: fositejwt
- pkg: github.com/go-jose/go-jose/v4/jwt
alias: josejwt
- pkg: github.com/go-jose/go-jose/v3
alias: oldjosev3
# Generated Pinniped
- pkg: go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1
alias: authenticationv1alpha1
- pkg: go.pinniped.dev/generated/latest/apis/supervisor/clientsecret/v1alpha1
alias: clientsecretv1alpha1
- pkg: go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1
alias: supervisorconfigv1alpha1
- pkg: go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1
alias: conciergeconfigv1alpha1
- pkg: go.pinniped.dev/generated/latest/client/concierge/clientset/versioned
alias: conciergeclientset
- pkg: go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/scheme
alias: conciergeclientsetscheme
- pkg: go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake
alias: conciergefake
- pkg: go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned
alias: supervisorclientset
- pkg: go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/scheme
alias: supervisorclientsetscheme
- pkg: go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake
alias: supervisorfake
- pkg: go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1
alias: idpv1alpha1
- pkg: go.pinniped.dev/generated/latest/client/concierge/informers/externalversions
alias: conciergeinformers
- pkg: go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions
alias: supervisorinformers
# Pinniped internal
- pkg: go.pinniped.dev/internal/concierge/scheme
alias: conciergescheme
gofmt:
# Simplify code: gofmt with `-s` option.
# Default: true
simplify: false
# Apply the rewrite rules to the source before reformatting.
# https://pkg.go.dev/cmd/gofmt
# Default: []
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
- pattern: 'a[b:len(a)]'
replacement: 'a[b:]'

View File

@@ -2,8 +2,8 @@
# On macOS, try `brew install pre-commit` and then run `pre-commit install`.
exclude: '^(site|generated)/'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
- repo: git://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
# TODO: find a version of this to validate ytt templates?
# - id: check-yaml
@@ -11,7 +11,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

View File

@@ -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
@@ -17,13 +14,24 @@ See [SCOPE.md](./SCOPE.md) for some guidelines about what we consider in and out
The near-term and mid-term roadmap for the work planned for the project [maintainers](MAINTAINERS.md) is documented in [ROADMAP.md](ROADMAP.md).
## Community Meetings
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. Please join us during our online community meetings,
occurring every first and third Thursday of the month at 9 AM PT / 12 PM ET.
Use [this Zoom Link](https://go.pinniped.dev/community/zoom)
to attend and add any agenda items you wish to discuss
to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view).
Join our [Google Group](https://groups.google.com/g/project-pinniped) to receive invites to this meeting.
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.
or in the Kubernetes Slack Workspace within the [#pinniped channel](https://kubernetes.slack.com/archives/C01BW364RJA).
## Issues
@@ -71,13 +79,6 @@ 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
@@ -114,6 +115,7 @@ go build -o pinniped ./cmd/pinniped
1. Install dependencies:
- [`chromedriver`](https://chromedriver.chromium.org/) (and [Chrome](https://www.google.com/chrome/))
- [`docker`](https://www.docker.com/)
- `htpasswd` (installed by default on MacOS, usually found in `apache2-utils` package for linux)
- [`kapp`](https://carvel.dev/#getting-started)
@@ -121,13 +123,11 @@ go build -o pinniped ./cmd/pinniped
- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [`ytt`](https://carvel.dev/#getting-started)
- [`nmap`](https://nmap.org/download.html)
- [`openssl`](https://www.openssl.org) (installed by default on MacOS)
- [Chrome](https://www.google.com/chrome/)
On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
```bash
brew install kind vmware-tanzu/carvel/ytt vmware-tanzu/carvel/kapp kubectl nmap && brew cask install docker
brew install kind k14s/tap/ytt k14s/tap/kapp kubectl chromedriver nmap && brew cask install docker
```
1. Create a kind cluster, compile, create container images, and install Pinniped and supporting test dependencies using:
@@ -139,56 +139,23 @@ go build -o pinniped ./cmd/pinniped
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 -timeout 0 ./test/integration
```
To run specific integration tests, add the `-run` flag to the above command to specify a regexp for the test names.
Use a leading `/` on the regexp because the Pinniped integration tests are automatically nested under several parent tests
(see [integration/main_test.go](https://github.com/vmware-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 destroy the local Kubernetes cluster, run `./hack/kind-down.sh`.
#### Using GoLand to Run an Integration Test
It can sometimes be convenient to use GoLand to run an integration test. For example, this allows using the
GoLand debugger to debug the test itself (not the server, since that it running in-cluster).
Note that the output of `hack/prepare-for-integration-tests.sh` says:
```bash
# Using GoLand? Paste the result of this command into GoLand's run configuration "Environment".
# hack/integration-test-env-goland.sh | pbcopy
```
After using `hack/prepare-for-integration-tests.sh`, run `hack/integration-test-env-goland.sh | pbcopy` as instructed. Then:
1. Select and run an integration test within GoLand. It will fail complaining about missing env vars.
1. Pull down the menu that shows the name of the test which you just ran in the previous step, and choose "Edit Configurations...".
1. In the "Environment" text box for the run configuration of the integration test that you just ran,
paste the results of `hack/integration-test-env-goland.sh | pbcopy`.
1. Apply, and then run the integration test again. This time the test will use the environment variables provided.
Note that if you run `hack/prepare-for-integration-tests.sh` again, then you may need to repeat these steps.
Each run of `hack/prepare-for-integration-tests.sh` can result in different values for some of the env vars.
### Observing Tests on the Continuous Integration Environment
[CI](https://ci.pinniped.dev/teams/main/pipelines/pull-requests)
[CI](https://hush-house.pivotal.io/teams/tanzu-user-auth/pipelines/pinniped-pull-requests)
will not be triggered on a pull request until the pull request is reviewed and
approved for CI by a project [maintainer](MAINTAINERS.md). Once CI is triggered,
the progress and results will appear on the Github page for that
[pull request](https://github.com/vmware-tanzu/pinniped/pulls) as checks. Links
will appear to view the details of each check.
## CI
Pinniped's CI configuration and code is in the [`ci`](https://github.com/vmware-tanzu/pinniped/tree/ci)
branch of this repo. The CI results are visible to the public at https://ci.pinniped.dev.
## Documentation
Any pull request which adds a new feature or changes the behavior of any feature which was previously documented

View File

@@ -1,54 +1,36 @@
# syntax=docker/dockerfile:1
# syntax = docker/dockerfile:1.0-experimental
# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
# Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
ARG BUILD_IMAGE=golang:1.23.4@sha256:585103a29aa6d4c98bbb45d2446e1fdf41441698bbdf707d1801f5708e479f04
ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:6ec5aa99dc335666e79dc64e4a6c8b89c33a543a1967f20d360922a80dd21f02
# Prepare to cross-compile by always running the build stage in the build platform, not the target platform.
FROM --platform=$BUILDPLATFORM $BUILD_IMAGE AS build-env
FROM golang:1.17.7 as build-env
WORKDIR /work
COPY . .
ARG GOPROXY
ARG KUBE_GIT_VERSION
ENV KUBE_GIT_VERSION=$KUBE_GIT_VERSION
# These will be set by buildkit automatically, e.g. TARGETOS set to "linux" and TARGETARCH set to "amd64" or "arm64".
# Useful for building multi-arch container images.
ARG TARGETOS
ARG TARGETARCH
# If provided, must be a comma-separated list of Go build tags.
ARG ADDITIONAL_BUILD_TAGS
# Build the statically linked (CGO_ENABLED=0) binary.
# Mount source, build cache, and module cache for performance reasons.
# See https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/
# 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=target=. \
--mount=type=cache,target=/cache/gocache \
--mount=type=cache,target=/cache/gomodcache \
export GOCACHE=/cache/gocache GOMODCACHE=/cache/gomodcache CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH && \
go build -tags $ADDITIONAL_BUILD_TAGS -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-concierge-kube-cert-agent ./cmd/pinniped-concierge-kube-cert-agent/... && \
go build -tags $ADDITIONAL_BUILD_TAGS -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-server ./cmd/pinniped-server/... && \
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
# Use a distroless runtime image with CA certificates, timezone data, and not much else.
# Note that we are not using --platform here, so it will choose the base image for the target platform, not the build platform.
# By using "distroless/static" instead of "distroless/static-debianXX" we can float on the latest stable version of debian.
# See https://github.com/GoogleContainerTools/distroless#base-operating-system
FROM $BASE_IMAGE
FROM gcr.io/distroless/static:nonroot@sha256:80c956fb0836a17a565c43a4026c9c80b2013c83bea09f74fa4da195a59b7a99
# Copy the server binary from the build-env stage.
COPY --from=build-env /usr/local/bin /usr/local/bin
# Document the default server ports for the various server apps
EXPOSE 8443 8444 10250
EXPOSE 8080 8443 8444 10250
# Run as non-root for security posture
# Use the same non-root user as https://github.com/GoogleContainerTools/distroless/blob/fc3c4eaceb0518900f886aae90407c43be0a42d9/base/base.bzl#L9

View File

@@ -1,18 +1,24 @@
# Current Pinniped Maintainers
# Pinniped Maintainers
| Maintainer | GitHub ID | Affiliation |
|-----------------|-----------------------------------------------------------|------------------------------------------|
| Ryan Richard | [cfryanr](https://github.com/cfryanr) | [VMware](https://www.github.com/vmware/) |
| Joshua T. Casey | [joshuatcasey](https://github.com/joshuatcasey) | [VMware](https://www.github.com/vmware/) |
This is the current list of maintainers for the Pinniped project.
| Maintainer | GitHub ID | Affiliation |
| --------------- | --------- | ----------- |
| Margo Crawford | [margocrawf](https://github.com/margocrawf) | [VMware](https://www.github.com/vmware/) |
| Mo Khan | [enj](https://github.com/enj) | [VMware](https://www.github.com/vmware/) |
| Anjali Telang | [anjaltelang](https://github.com/anjaltelang) | [VMware](https://www.github.com/vmware/) |
| Ryan Richard | [cfryanr](https://github.com/cfryanr) | [VMware](https://www.github.com/vmware/) |
## Emeritus Maintainers
| Maintainer | GitHub ID |
|-------------------|-----------------------------------------------------------|
| Andrew Keesler | [ankeesler](https://github.com/ankeesler) |
| Anjali Telang | [anjaltelang](https://github.com/anjaltelang) |
| Ben Petersen | [benjaminapetersen](https://github.com/benjaminapetersen) |
| Margo Crawford | [margocrawf](https://github.com/margocrawf) |
| Matt Moyer | [mattmoyer](https://github.com/mattmoyer) |
| Mo Khan | [enj](https://github.com/enj) |
| Pablo Schuhmacher | [pabloschuhmacher](https://github.com/pabloschuhmacher) |
* Andrew Keesler, [ankeesler](https://github.com/ankeesler)
* Pablo Schuhmacher, [pabloschuhmacher](https://github.com/pabloschuhmacher)
* Matt Moyer, [mattmoyer](https://github.com/mattmoyer)
## Pinniped Contributors & Stakeholders
| Feature Area | Lead |
| ----------------------------- | :---------------------: |
| Technical Lead | Mo Khan (enj) |
| Product Management | Anjali Telang (anjaltelang) |
| Community Management | Nanci Lancaster (microwavables) |

View File

@@ -21,19 +21,29 @@ Care to kick the tires? It's easy to [install and try Pinniped](https://pinniped
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.
or in the Kubernetes Slack Workspace within the [#pinniped channel](https://kubernetes.slack.com/archives/C01BW364RJA).
## Contributions
Pinniped is better because of our contributors and [maintainers](MAINTAINERS.md). It is because of you that we can bring
great software to the community.
Want to get involved? Contributions are welcome.
Please see the [contributing guide](CONTRIBUTING.md) for more information about reporting bugs, requesting features,
building and testing the code, submitting PRs, and other contributor topics.
## Community meetings
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. Please join us during our online community meetings, occurring every first and third
Thursday of the month at 9 AM PT / 12 PM ET.
**Note:** Community meetings are currently paused until early 2022 as we wind down 2021!
Use [this Zoom Link](https://go.pinniped.dev/community/zoom) to attend and add any agenda items you wish to
discuss to [the notes document](https://go.pinniped.dev/community/agenda).
Join our [Google Group](https://groups.google.com/g/project-pinniped) to receive invites to this meeting.
If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled.
## Adopters
Some organizations and products using Pinniped are featured in [ADOPTERS.md](ADOPTERS.md).
@@ -47,4 +57,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-2024 the Pinniped contributors. All Rights Reserved.
Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.

View File

@@ -1,26 +1,68 @@
## Pinniped Project Roadmap
### About this document
## **Pinniped Project Roadmap**
This document provides a high-level overview of the next big features the maintainers are planning to work on. This
should serve as a reference point for Pinniped users and contributors to understand where the project is heading, and
help determine if a contribution could be conflicting with a longer term plan.
### How to help
###
**About this document**
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.
This document provides a link to the[ Pinniped Project issues](https://github.com/vmware-tanzu/pinniped/issues) list that serves as the up to date description of items that are in the Pinniped release pipeline. Most items are gathered from the community or include a feedback loop with the community. This should serve as a reference point for Pinniped users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
### How to 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.
###
**How to help?**
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.
Discussion on the roadmap can take place in threads under [Issues](https://github.com/vmware-tanzu/pinniped/issues) or in [community meetings](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md#meeting-with-the-maintainers). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
###
**Need an idea for a contribution?**
Weve created an [Opportunity Areas](https://github.com/vmware-tanzu/pinniped/discussions/483) discussion thread that outlines some areas we believe are excellent starting points for the community to get involved. In that discussion weve included specific work items that one might consider that also support the high-level items presented in our roadmap.
###
**How to add an item to the roadmap?**
Please open an issue to track any initiative on the roadmap of Pinniped (usually driven by new feature requests). We will work with and rely on our community to focus our efforts to improve Pinniped.
###
**Current Roadmap**
The following table includes the current roadmap for Pinniped. If you have any questions or would like to contribute to Pinniped, please attend a [community meeting](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md#meeting-with-the-maintainers) to discuss with our team. If you don't know where to start, we are always looking for contributors that will help us reduce technical, automation, and documentation debt. Please take the timelines & 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 attend community meetings to get updated status information, or help us deliver that feature by contributing to Pinniped.
Last Updated: Jan 2022
|Theme|Description|Timeline|
|--|--|--|
|Improving Security Posture|Support for refreshing LDAP/AD Group information |March 2022|
|Improving Security Posture|Support FIPS compliant Boring crypto libraries |March/April 2022|
|Multiple IDP support|Support multiple IDPs configured on a single Supervisor|April 2022|
|Improving Security Posture|TLS hardening |March/April 2022|
|Improving Security Posture|Support Audit logging of security events related to Authentication |April/May 2022|
|Improving Usability|Support for integrating with UI/Dashboards |June/July 2022|
|Improving Security Posture|mTLS for Supervisor sessions |Exploring/Ongoing|
|Improving Security Posture|Key management/rotation for Pinniped components with minimal downtime |Exploring/Ongoing|
|Improving Security Posture|Support for Session Logout |Exploring/Ongoing|
|Improving Security Posture|Support for Idle Session/ Inactivity timeout|Exploring/Ongoing|
|Improving Security Posture|Support for Max Concurrent Sessions|Exploring/Ongoing|
|Improving Security Posture|Support for configurable Session Length |Exploring/Ongoing|
|Improving Security Posture|Reject use of username and groups with system: prefix |Exploring/Ongoing|
|Improving Security Posture|Support for using external KMS for Supervisor signing keys |Exploring/Ongoing|
|Improving Security Posture|Client side use of Secure Enclaves for Session data |Exploring/Ongoing|
|Improving Security Posture|Enforce the use of HTTP Strict Transport (HSTS) |Exploring/Ongoing|
|Improving Security Posture|Assert that Pinniped runs under the restricted PSP version2 levels |Exploring/Ongoing|
|Wider Concierge cluster support|Support for OpenShift cluster types in the Concierge|Exploring/Ongoing|
|Identity transforms|Support prefixing, filtering, or performing coarse-grained checks on upstream users and groups|Exploring/Ongoing|
|CLI SSO|Support Kerberos based authentication on CLI |Exploring/Ongoing|
|Extended IDP support|Support more types of identity providers on the Supervisor|Exploring/Ongoing|
|Improved Documentation|Reorganizing and improving Pinniped docs; new how-to guides and tutorials|Exploring/Ongoing|
|Improve our CI/CD systems|Upgrade tests; make Kind more efficient and reliable for CI ; Windows tests; performance tests; scale tests; soak tests|Exploring/Ongoing|
|CLI Improvements|Improving CLI UX for setting up Supervisor IDPs|Exploring/Ongoing|
|Telemetry|Adding some useful phone home metrics as well as some vanity metrics|Exploring/Ongoing|
|Observability|Expose Pinniped metrics through Prometheus Integration|Exploring/Ongoing|
|Device Code Flow|Add support for OAuth 2.0 Device Authorization Grant in the Pinniped CLI and Supervisor|Exploring/Ongoing|
|Supervisor with New Clients|Enable registering new clients with Supervisor|Exploring/Ongoing|

View File

@@ -21,3 +21,12 @@ The following items are out of scope for the Pinniped project.
- 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

View File

@@ -1,7 +1,9 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package
// +k8s:defaulter-gen=TypeMeta
// +groupName=authentication.concierge.pinniped.dev
// Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authentication API.

View File

@@ -1,23 +1,10 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-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 JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -25,11 +12,7 @@ type JWTAuthenticatorStatus struct {
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}
// Spec for configuring a JWT authenticator.
@@ -79,7 +62,6 @@ type JWTTokenClaims struct {
// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
// +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience`
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:subresource:status
type JWTAuthenticator struct {

View File

@@ -0,0 +1,75 @@
// Copyright 2020-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"`
}

View File

@@ -1,47 +1,11 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles.
//
// +kubebuilder:validation:Enum=Secret;ConfigMap
type CertificateAuthorityDataSourceKind string
const (
// CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles.
CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap")
// CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles.
// Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque.
CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret")
)
// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification.
type CertificateAuthorityDataSourceSpec struct {
// Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
// Allowed values are "Secret" or "ConfigMap".
// "ConfigMap" uses a Kubernetes configmap to source CA Bundles.
// "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
Kind CertificateAuthorityDataSourceKind `json:"kind"`
// Name is the resource name of the secret or configmap from which to read the CA bundle.
// The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// Key is the key name within the secret or configmap from which to read the CA bundle.
// The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
// certificate bundle.
// +kubebuilder:validation:MinLength=1
Key string `json:"key"`
}
// TLSSpec provides TLS configuration on various authenticators.
// Configuration for configuring TLS on various authenticators.
type TLSSpec struct {
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
// +optional
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
// Reference to a CA bundle in a secret or a configmap.
// Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
// +optional
CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"`
}

View File

@@ -1,23 +1,10 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-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 WebhookAuthenticatorPhase string
const (
// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
)
// Status of a webhook authenticator.
type WebhookAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -25,11 +12,7 @@ type WebhookAuthenticatorStatus struct {
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the WebhookAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}
// Spec for configuring a webhook authenticator.
@@ -50,7 +33,6 @@ type WebhookAuthenticatorSpec struct {
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint`
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:subresource:status
type WebhookAuthenticator struct {

View File

@@ -1,7 +1,9 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package
// +k8s:defaulter-gen=TypeMeta
// +groupName=config.concierge.pinniped.dev
// Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration API.

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -49,7 +49,6 @@ type CredentialIssuerSpec struct {
}
// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy.
// Allowed values are "auto", "enabled", or "disabled".
//
// +kubebuilder:validation:Enum=auto;enabled;disabled
type ImpersonationProxyMode string
@@ -66,7 +65,6 @@ const (
)
// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy.
// Allowed values are "LoadBalancer", "ClusterIP", or "None".
//
// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None
type ImpersonationProxyServiceType string
@@ -82,28 +80,6 @@ const (
ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None")
)
// ImpersonationProxyTLSSpec contains information about how the Concierge impersonation proxy should
// serve TLS.
//
// If CertificateAuthorityData is not provided, the Concierge impersonation proxy will check the secret
// for a field called "ca.crt", which will be used as the CertificateAuthorityData.
//
// If neither CertificateAuthorityData nor ca.crt is provided, no CA bundle will be advertised for
// the impersonation proxy endpoint.
type ImpersonationProxyTLSSpec struct {
// X.509 Certificate Authority (base64-encoded PEM bundle).
// Used to advertise the CA bundle for the impersonation proxy endpoint.
//
// +optional
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
// SecretName is the name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains
// the TLS serving certificate for the Concierge impersonation proxy endpoint.
//
// +kubebuilder:validation:MinLength=1
SecretName string `json:"secretName,omitempty"`
}
// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy.
type ImpersonationProxySpec struct {
// Mode configures whether the impersonation proxy should be started:
@@ -124,13 +100,6 @@ type ImpersonationProxySpec struct {
//
// +optional
ExternalEndpoint string `json:"externalEndpoint,omitempty"`
// TLS contains information about how the Concierge impersonation proxy should serve TLS.
//
// If this field is empty, the impersonation proxy will generate its own TLS certificate.
//
// +optional
TLS *ImpersonationProxyTLSSpec `json:"tls,omitempty"`
}
// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy.

View File

@@ -0,0 +1,40 @@
// 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
}
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
}
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
}

View File

@@ -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
}

View File

@@ -0,0 +1,43 @@
// 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"`
}
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
}
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"`
}

View File

@@ -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"`
}

View File

@@ -0,0 +1,21 @@
// Copyright 2020-2022 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 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
// Token is a bearer token used by the client for request authentication.
Token string
// PEM-encoded client TLS certificates (including intermediates, if any).
ClientCertificateData string
// PEM-encoded private key for the above certificate.
ClientKeyData string
}

View File

@@ -1,22 +0,0 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// ClusterCredential is the cluster-specific credential returned on a successful credential request. It
// contains either a valid bearer token or a valid TLS certificate and corresponding private key for the cluster.
type ClusterCredential struct {
// ExpirationTimestamp indicates a time when the provided credentials expire.
ExpirationTimestamp metav1.Time
// Token is a bearer token used by the client for request authentication.
Token string
// PEM-encoded client TLS certificates (including intermediates, if any).
ClientCertificateData string
// PEM-encoded private key for the above certificate.
ClientKeyData string
}

View File

@@ -0,0 +1,47 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type TokenCredentialRequestSpec struct {
// Bearer token supplied with the credential request.
Token string
// Reference to an authenticator which can validate this credential request.
Authenticator corev1.TypedLocalObjectReference
}
type TokenCredentialRequestStatus struct {
// A ClusterCredential will be returned for a successful credential request.
// +optional
Credential *ClusterCredential
// An error message will be returned for an unsuccessful credential request.
// +optional
Message *string
}
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TokenCredentialRequest struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec TokenCredentialRequestSpec
Status TokenCredentialRequestStatus
}
// TokenCredentialRequestList is a list of TokenCredentialRequest objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TokenCredentialRequestList struct {
metav1.TypeMeta
metav1.ListMeta
// Items is a list of TokenCredentialRequest
Items []TokenCredentialRequest
}

View File

@@ -1,49 +0,0 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Specification of a TokenCredentialRequest, expected on requests to the Pinniped API.
type TokenCredentialRequestSpec struct {
// Bearer token supplied with the credential request.
Token string
// Reference to an authenticator which can validate this credential request.
Authenticator corev1.TypedLocalObjectReference
}
// Status of a TokenCredentialRequest, returned on responses to the Pinniped API.
type TokenCredentialRequestStatus struct {
// A Credential will be returned for a successful credential request.
// +optional
Credential *ClusterCredential
// An error message will be returned for an unsuccessful credential request.
// +optional
Message *string
}
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TokenCredentialRequest struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec TokenCredentialRequestSpec
Status TokenCredentialRequestStatus
}
// TokenCredentialRequestList is a list of TokenCredentialRequest objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TokenCredentialRequestList struct {
metav1.TypeMeta
metav1.ListMeta
// Items is a list of TokenCredentialRequest.
Items []TokenCredentialRequest
}

View File

@@ -0,0 +1,51 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// 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"`
// Reference to an authenticator which can validate this credential request.
Authenticator corev1.TypedLocalObjectReference `json:"authenticator"`
}
// 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
Credential *ClusterCredential `json:"credential,omitempty"`
// An error message will be returned for an unsuccessful credential request.
// +optional
Message *string `json:"message,omitempty"`
}
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
// +genclient
// +genclient:nonNamespaced
// +genclient:onlyVerbs=create
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TokenCredentialRequest struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TokenCredentialRequestSpec `json:"spec,omitempty"`
Status TokenCredentialRequestStatus `json:"status,omitempty"`
}
// TokenCredentialRequestList is a list of TokenCredentialRequest objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TokenCredentialRequestList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TokenCredentialRequest `json:"items"`
}

View File

@@ -1,52 +0,0 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Specification of a TokenCredentialRequest, expected on requests to the Pinniped API.
type TokenCredentialRequestSpec struct {
// Bearer token supplied with the credential request.
Token string `json:"token,omitempty"`
// Reference to an authenticator which can validate this credential request.
Authenticator corev1.TypedLocalObjectReference `json:"authenticator"`
}
// Status of a TokenCredentialRequest, returned on responses to the Pinniped API.
type TokenCredentialRequestStatus struct {
// A Credential will be returned for a successful credential request.
// +optional
Credential *ClusterCredential `json:"credential,omitempty"`
// An error message will be returned for an unsuccessful credential request.
// +optional
Message *string `json:"message,omitempty"`
}
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
// +genclient
// +genclient:nonNamespaced
// +genclient:onlyVerbs=create
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TokenCredentialRequest struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TokenCredentialRequestSpec `json:"spec,omitempty"`
Status TokenCredentialRequestStatus `json:"status,omitempty"`
}
// TokenCredentialRequestList is a list of TokenCredentialRequest objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TokenCredentialRequestList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
// Items is a list of TokenCredentialRequest.
Items []TokenCredentialRequest `json:"items"`
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,4 +0,0 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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"`
}

View File

@@ -1,7 +1,10 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-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/config
// +k8s:defaulter-gen=TypeMeta
// +groupName=config.supervisor.pinniped.dev
// Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuration API.

View File

@@ -32,8 +32,6 @@ func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&FederationDomain{},
&FederationDomainList{},
&OIDCClient{},
&OIDCClientList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -8,17 +8,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type FederationDomainPhase string
// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret
type FederationDomainStatusCondition string
const (
// FederationDomainPhasePending is the default phase for newly-created FederationDomain resources.
FederationDomainPhasePending FederationDomainPhase = "Pending"
// FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state.
FederationDomainPhaseReady FederationDomainPhase = "Ready"
// FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state.
FederationDomainPhaseError FederationDomainPhase = "Error"
SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success")
DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate")
SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret")
InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid")
)
// FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider.
@@ -34,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.
@@ -45,159 +41,6 @@ type FederationDomainTLSSpec struct {
SecretName string `json:"secretName,omitempty"`
}
// FederationDomainTransformsConstant defines a constant variable and its value which will be made available to
// the transform expressions. This is a union type, and Type is the discriminator field.
type FederationDomainTransformsConstant struct {
// Name determines the name of the constant. It must be a valid identifier name.
// +kubebuilder:validation:Pattern=`^[a-zA-Z][_a-zA-Z0-9]*$`
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=64
Name string `json:"name"`
// Type determines the type of the constant, and indicates which other field should be non-empty.
// Allowed values are "string" or "stringList".
// +kubebuilder:validation:Enum=string;stringList
Type string `json:"type"`
// StringValue should hold the value when Type is "string", and is otherwise ignored.
// +optional
StringValue string `json:"stringValue,omitempty"`
// StringListValue should hold the value when Type is "stringList", and is otherwise ignored.
// +optional
StringListValue []string `json:"stringListValue,omitempty"`
}
// FederationDomainTransformsExpression defines a transform expression.
type FederationDomainTransformsExpression struct {
// Type determines the type of the expression. It must be one of the supported types.
// Allowed values are "policy/v1", "username/v1", or "groups/v1".
// +kubebuilder:validation:Enum=policy/v1;username/v1;groups/v1
Type string `json:"type"`
// Expression is a CEL expression that will be evaluated based on the Type during an authentication.
// +kubebuilder:validation:MinLength=1
Expression string `json:"expression"`
// Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects
// an authentication attempt. When empty, a default message will be used.
// +optional
Message string `json:"message,omitempty"`
}
// FederationDomainTransformsExample defines a transform example.
type FederationDomainTransformsExample struct {
// Username is the input username.
// +kubebuilder:validation:MinLength=1
Username string `json:"username"`
// Groups is the input list of group names.
// +optional
Groups []string `json:"groups,omitempty"`
// Expects is the expected output of the entire sequence of transforms when they are run against the
// input Username and Groups.
Expects FederationDomainTransformsExampleExpects `json:"expects"`
}
// FederationDomainTransformsExampleExpects defines the expected result for a transforms example.
type FederationDomainTransformsExampleExpects struct {
// Username is the expected username after the transformations have been applied.
// +optional
Username string `json:"username,omitempty"`
// Groups is the expected list of group names after the transformations have been applied.
// +optional
Groups []string `json:"groups,omitempty"`
// Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression
// after the transformations have been applied. True means that it is expected that the authentication would be
// rejected. The default value of false means that it is expected that the authentication would not be rejected
// by any policy expression.
// +optional
Rejected bool `json:"rejected,omitempty"`
// Message is the expected error message of the transforms. When Rejected is true, then Message is the expected
// message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank,
// then Message will be treated as the default error message for authentication attempts which are rejected by a
// policy. When Rejected is false, then Message is the expected error message for some other non-policy
// transformation error, such as a runtime error. When Rejected is false, there is no default expected Message.
// +optional
Message string `json:"message,omitempty"`
}
// FederationDomainTransforms defines identity transformations for an identity provider's usage on a FederationDomain.
type FederationDomainTransforms struct {
// Constants defines constant variables and their values which will be made available to the transform expressions.
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
// +optional
Constants []FederationDomainTransformsConstant `json:"constants,omitempty"`
// Expressions are an optional list of transforms and policies to be executed in the order given during every
// authentication attempt, including during every session refresh.
// Each is a CEL expression. It may use the basic CEL language as defined in
// https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in
// https://github.com/google/cel-go/tree/master/ext#strings.
//
// The username and groups extracted from the identity provider, and the constants defined in this CR, are
// available as variables in all expressions. The username is provided via a variable called `username` and
// the list of group names is provided via a variable called `groups` (which may be an empty list).
// Each user-provided constants is provided via a variable named `strConst.varName` for string constants
// and `strListConst.varName` for string list constants.
//
// The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1.
// Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated
// and the authentication attempt is rejected.
// Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the
// username or group names.
// Each username/v1 transform must return the new username (a string), which can be the same as the old username.
// Transformations of type username/v1 do not return group names, and therefore cannot change the group names.
// Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old
// groups list.
// Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames.
// After each expression, the new (potentially changed) username or groups get passed to the following expression.
//
// Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain.
// During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the
// authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username
// and group names have been decided for that authentication attempt.
//
// +optional
Expressions []FederationDomainTransformsExpression `json:"expressions,omitempty"`
// Examples can optionally be used to ensure that the sequence of transformation expressions are working as
// expected. Examples define sample input identities which are then run through the expression list, and the
// results are compared to the expected results. If any example in this list fails, then this
// identity provider will not be available for use within this FederationDomain, and the error(s) will be
// added to the FederationDomain status. This can be used to help guard against programming mistakes in the
// expressions, and also act as living documentation for other administrators to better understand the expressions.
// +optional
Examples []FederationDomainTransformsExample `json:"examples,omitempty"`
}
// FederationDomainIdentityProvider describes how an identity provider is made available in this FederationDomain.
type FederationDomainIdentityProvider struct {
// DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the
// kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a
// disruptive change for those users.
// +kubebuilder:validation:MinLength=1
DisplayName string `json:"displayName"`
// ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required.
// If the reference cannot be resolved then the identity provider will not be made available.
// Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider,
// LDAPIdentityProvider, ActiveDirectoryIdentityProvider.
ObjectRef corev1.TypedLocalObjectReference `json:"objectRef"`
// Transforms is an optional way to specify transformations to be applied during user authentication and
// session refresh.
// +optional
Transforms FederationDomainTransforms `json:"transforms,omitempty"`
}
// FederationDomainSpec is a struct that describes an OIDC Provider.
type FederationDomainSpec struct {
// Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the
@@ -211,35 +54,9 @@ type FederationDomainSpec struct {
// +kubebuilder:validation:MinLength=1
Issuer string `json:"issuer"`
// TLS specifies a secret which will contain Transport Layer Security (TLS) configuration for the FederationDomain.
// TLS configures how this FederationDomain is served over Transport Layer Security (TLS).
// +optional
TLS *FederationDomainTLSSpec `json:"tls,omitempty"`
// IdentityProviders is the list of identity providers available for use by this FederationDomain.
//
// An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server,
// how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to
// extract a normalized user identity. Normalized user identities include a username and a list of group names.
// In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which
// belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations
// on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid
// accidental conflicts when multiple identity providers have different users with the same username (e.g.
// "idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication
// rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow
// the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could
// disallow the authentication unless the user belongs to a specific group in the identity provider.
//
// For backwards compatibility with versions of Pinniped which predate support for multiple identity providers,
// an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which
// exist in the same namespace, but also to reject all authentication requests when there is more than one identity
// provider currently defined. In this backwards compatibility mode, the name of the identity provider resource
// (e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this
// FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of
// relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead
// explicitly list the identity provider using this IdentityProviders field.
//
// +optional
IdentityProviders []FederationDomainIdentityProvider `json:"identityProviders,omitempty"`
}
// FederationDomainSecrets holds information about this OIDC Provider's secrets.
@@ -268,17 +85,20 @@ type FederationDomainSecrets struct {
// FederationDomainStatus is a struct that describes the actual state of an OIDC Provider.
type FederationDomainStatus struct {
// Phase summarizes the overall status of the FederationDomain.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase FederationDomainPhase `json:"phase,omitempty"`
// Status holds an enum that describes the state of this OIDC Provider. Note that this Status can
// represent success or failure.
// +optional
Status FederationDomainStatusCondition `json:"status,omitempty"`
// Conditions represent the observations of an FederationDomain's current state.
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Message provides human-readable details about the Status.
// +optional
Message string `json:"message,omitempty"`
// LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get
// around some undesirable behavior with respect to the empty metav1.Time value (see
// https://github.com/kubernetes/kubernetes/issues/86811).
// +optional
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
// Secrets contains information about this OIDC Provider's secrets.
// +optional
@@ -290,7 +110,7 @@ type FederationDomainStatus struct {
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:categories=pinniped
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:subresource:status
type FederationDomain struct {

View File

@@ -1,144 +0,0 @@
// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type OIDCClientPhase string
const (
// OIDCClientPhasePending is the default phase for newly-created OIDCClient resources.
OIDCClientPhasePending OIDCClientPhase = "Pending"
// OIDCClientPhaseReady is the phase for an OIDCClient resource in a healthy state.
OIDCClientPhaseReady OIDCClientPhase = "Ready"
// OIDCClientPhaseError is the phase for an OIDCClient in an unhealthy state.
OIDCClientPhaseError OIDCClientPhase = "Error"
)
// +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/`
type RedirectURI string
// +kubebuilder:validation:Enum="authorization_code";"refresh_token";"urn:ietf:params:oauth:grant-type:token-exchange"
type GrantType string
// +kubebuilder:validation:Enum="openid";"offline_access";"username";"groups";"pinniped:request-audience"
type Scope string
// OIDCClientSpec is a struct that describes an OIDCClient.
type OIDCClientSpec struct {
// allowedRedirectURIs is a list of the allowed redirect_uri param values that should be accepted during OIDC flows with this
// client. Any other uris will be rejected.
// Must be a URI with the https scheme, unless the hostname is 127.0.0.1 or ::1 which may use the http scheme.
// Port numbers are not required for 127.0.0.1 or ::1 and are ignored when checking for a matching redirect_uri.
// +listType=set
// +kubebuilder:validation:MinItems=1
AllowedRedirectURIs []RedirectURI `json:"allowedRedirectURIs"`
// allowedGrantTypes is a list of the allowed grant_type param values that should be accepted during OIDC flows with this
// client.
//
// Must only contain the following values:
// - authorization_code: allows the client to perform the authorization code grant flow, i.e. allows the webapp to
// authenticate users. This grant must always be listed.
// - refresh_token: allows the client to perform refresh grants for the user to extend the user's session.
// This grant must be listed if allowedScopes lists offline_access.
// - urn:ietf:params:oauth:grant-type:token-exchange: allows the client to perform RFC8693 token exchange,
// which is a step in the process to be able to get a cluster credential for the user.
// This grant must be listed if allowedScopes lists pinniped:request-audience.
// +listType=set
// +kubebuilder:validation:MinItems=1
AllowedGrantTypes []GrantType `json:"allowedGrantTypes"`
// allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client.
//
// Must only contain the following values:
// - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat).
// This scope must always be listed.
// - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow.
// This scope must be listed if allowedGrantTypes lists refresh_token.
// - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange,
// which is a step in the process to be able to get a cluster credential for the user.
// openid, username and groups scopes must be listed when this scope is present.
// This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange.
// - username: The client is allowed to request that ID tokens contain the user's username.
// Without the username scope being requested and allowed, the ID token will not contain the user's username.
// - groups: The client is allowed to request that ID tokens contain the user's group membership,
// if their group membership is discoverable by the Supervisor.
// Without the groups scope being requested and allowed, the ID token will not contain groups.
// +listType=set
// +kubebuilder:validation:MinItems=1
AllowedScopes []Scope `json:"allowedScopes"`
// tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient.
// +optional
TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"`
}
// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient.
type OIDCClientTokenLifetimes struct {
// idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of
// ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID
// tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used.
// This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens
// short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the
// external identity provider to decide if it is acceptable for the end user to continue their session, and will
// update the end user's group memberships from the external identity provider. Giving these tokens a long life is
// will allow the end user to continue to use a token while avoiding these updates from the external identity
// provider. However, some web applications may have reasons specific to the design of that application to prefer
// longer lifetimes.
// +kubebuilder:validation:Minimum=120
// +kubebuilder:validation:Maximum=1800
// +optional
IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"`
}
// OIDCClientStatus is a struct that describes the actual state of an OIDCClient.
type OIDCClientStatus struct {
// phase summarizes the overall status of the OIDCClient.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase OIDCClientPhase `json:"phase,omitempty"`
// conditions represent the observations of an OIDCClient's current state.
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// totalClientSecrets is the current number of client secrets that are detected for this OIDCClient.
// +optional
TotalClientSecrets int32 `json:"totalClientSecrets"` // do not omitempty to allow it to show in the printer column even when it is 0
}
// OIDCClient describes the configuration of an OIDC client.
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:categories=pinniped
// +kubebuilder:printcolumn:name="Privileged Scopes",type=string,JSONPath=`.spec.allowedScopes[?(@ == "pinniped:request-audience")]`
// +kubebuilder:printcolumn:name="Client Secrets",type=integer,JSONPath=`.status.totalClientSecrets`
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:subresource:status
type OIDCClient struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// Spec of the OIDC client.
Spec OIDCClientSpec `json:"spec"`
// Status of the OIDC client.
Status OIDCClientStatus `json:"status,omitempty"`
}
// List of OIDCClient objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type OIDCClientList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []OIDCClient `json:"items"`
}

View File

@@ -1,7 +1,9 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package
// +k8s:defaulter-gen=TypeMeta
// +groupName=idp.supervisor.pinniped.dev
// +groupGoName=IDP

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -36,8 +36,6 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&LDAPIdentityProviderList{},
&ActiveDirectoryIdentityProvider{},
&ActiveDirectoryIdentityProviderList{},
&GitHubIdentityProvider{},
&GitHubIdentityProviderList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -32,7 +32,7 @@ type ActiveDirectoryIdentityProviderStatus struct {
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}
type ActiveDirectoryIdentityProviderBind struct {
@@ -114,10 +114,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct {
// Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user.
// The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the
// value of an attribute of the user entry found as a result of the user search. Which attribute's
// value is used to replace the placeholder(s) depends on the value of UserAttributeForFilter.
// E.g. "member={}" or "&(objectClass=groupOfNames)(member={})".
// For more information about ActiveDirectory filters, see https://ldap.com/ldap-filters.
// 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:={})".
@@ -128,17 +127,6 @@ type ActiveDirectoryIdentityProviderGroupSearch struct {
// +optional
Filter string `json:"filter,omitempty"`
// UserAttributeForFilter specifies which attribute's value from the user entry found as a result of
// the user search will be used to replace the "{}" placeholder(s) in the group search Filter.
// For example, specifying "uid" as the UserAttributeForFilter while specifying
// "&(objectClass=posixGroup)(memberUid={})" as the Filter would search for groups by replacing
// the "{}" placeholder in the Filter with the value of the user's "uid" attribute.
// Optional. When not specified, the default will act as if "dn" were specified. For example, leaving
// UserAttributeForFilter unspecified while specifying "&(objectClass=groupOfNames)(member={})" as the Filter
// would search for groups by replacing the "{}" placeholder(s) with the dn (distinguished name) of the user.
// +optional
UserAttributeForFilter string `json:"userAttributeForFilter,omitempty"`
// Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as
// the result of the group search.
// +optional

View File

@@ -1,263 +0,0 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type GitHubIdentityProviderPhase string
const (
// GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources.
GitHubPhasePending GitHubIdentityProviderPhase = "Pending"
// GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state.
GitHubPhaseReady GitHubIdentityProviderPhase = "Ready"
// GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state.
GitHubPhaseError GitHubIdentityProviderPhase = "Error"
)
type GitHubAllowedAuthOrganizationsPolicy string
const (
// GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity
// provider, regardless of their organization membership or lack thereof.
GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers"
// GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in
// the listed GitHub organizations are allowed to log in.
GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations"
)
// GitHubIdentityProviderStatus is the status of an GitHub identity provider.
type GitHubIdentityProviderStatus struct {
// Phase summarizes the overall status of the GitHubIdentityProvider.
//
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase GitHubIdentityProviderPhase `json:"phase,omitempty"`
// Conditions represents the observations of an identity provider's current state.
//
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}
// GitHubAPIConfig allows configuration for GitHub Enterprise Server
type GitHubAPIConfig struct {
// Host is required only for GitHub Enterprise Server.
// Defaults to using GitHub's public API ("github.com").
// For convenience, specifying "github.com" is equivalent to specifying "api.github.com".
// Do not specify a protocol or scheme since "https://" will always be used.
// Port is optional. Do not specify a path, query, fragment, or userinfo.
// Only specify domain name or IP address, subdomains (optional), and port (optional).
// IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address
// in square brackets. Example: "[::1]:443".
//
// +kubebuilder:default="github.com"
// +kubebuilder:validation:MinLength=1
// +optional
Host *string `json:"host"`
// TLS configuration for GitHub Enterprise Server.
// Note that this field should not be needed when using GitHub's public API ("github.com").
// However, if you choose to specify this field when using GitHub's public API, you must
// specify a CA bundle that will verify connections to "api.github.com".
//
// +optional
TLS *TLSSpec `json:"tls,omitempty"`
}
// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present
// to Kubernetes. See the response schema for
// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user).
type GitHubUsernameAttribute string
const (
// GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes.
GitHubUsernameID GitHubUsernameAttribute = "id"
// GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes.
GitHubUsernameLogin GitHubUsernameAttribute = "login"
// GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the
// username to present to Kubernetes, separated by a colon. Example: "my-login:1234"
GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id"
)
// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group
// names to present to Kubernetes. See the response schema for
// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user).
type GitHubGroupNameAttribute string
const (
// GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes.
GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name"
// GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes.
GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug"
)
// GitHubClaims allows customization of the username and groups claims.
type GitHubClaims struct {
// Username configures which property of the GitHub user record shall determine the username in Kubernetes.
//
// Can be either "id", "login", or "login:id". Defaults to "login:id".
//
// GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens,
// and may not start or end with hyphens. GitHub users are allowed to change their login name,
// although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar",
// then a second user might change their name from "baz" to "foo" in order to take the old
// username of the first user. For this reason, it is not as safe to make authorization decisions
// based only on the user's login attribute.
//
// If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's
// FederationDomain to further customize how these usernames are presented to Kubernetes.
//
// Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and
// unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value
// from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable
// choice to concatenate the two values.
//
// See the response schema for
// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user).
//
// +kubebuilder:default="login:id"
// +kubebuilder:validation:Enum={"id","login","login:id"}
// +optional
Username *GitHubUsernameAttribute `json:"username"`
// Groups configures which property of the GitHub team record shall determine the group names in Kubernetes.
//
// Can be either "name" or "slug". Defaults to "slug".
//
// GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!").
//
// GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins").
//
// Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a
// forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters
// or single hyphens, so the first forward slash `/` will be the separator between the organization login name and
// the team name or slug.
//
// If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's
// FederationDomain to further customize how these group names are presented to Kubernetes.
//
// See the response schema for
// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user).
//
// +kubebuilder:default=slug
// +kubebuilder:validation:Enum=name;slug
// +optional
Groups *GitHubGroupNameAttribute `json:"groups"`
}
// GitHubClientSpec contains information about the GitHub client that this identity provider will use
// for web-based login flows.
type GitHubClientSpec struct {
// SecretName contains the name of a namespace-local Secret object that provides the clientID and
// clientSecret for an GitHub App or GitHub OAuth2 client.
//
// This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret".
//
// +kubebuilder:validation:MinLength=1
SecretName string `json:"secretName"`
}
type GitHubOrganizationsSpec struct {
// Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers".
// Defaults to "OnlyUsersFromAllowedOrganizations".
//
// Must be set to "AllGitHubUsers" if the allowed field is empty.
//
// This field only exists to ensure that Pinniped administrators are aware that an empty list of
// allowedOrganizations means all GitHub users are allowed to log in.
//
// +kubebuilder:default=OnlyUsersFromAllowedOrganizations
// +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers
// +optional
Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"`
// Allowed, when specified, indicates that only users with membership in at least one of the listed
// GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include
// teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be
// provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP.
//
// The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations,
// otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams
// within that organization.
//
// If no organizations are listed, you must set organizations: AllGitHubUsers.
//
// +kubebuilder:validation:MaxItems=64
// +listType=set
// +optional
Allowed []string `json:"allowed,omitempty"`
}
// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how.
type GitHubAllowAuthenticationSpec struct {
// Organizations allows customization of which organizations can authenticate using this IDP.
// +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')"
// +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')"
Organizations GitHubOrganizationsSpec `json:"organizations"`
}
// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider.
type GitHubIdentityProviderSpec struct {
// GitHubAPI allows configuration for GitHub Enterprise Server
//
// +kubebuilder:default={}
GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"`
// Claims allows customization of the username and groups claims.
//
// +kubebuilder:default={}
Claims GitHubClaims `json:"claims,omitempty"`
// AllowAuthentication allows customization of who can authenticate using this IDP and how.
AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"`
// Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client).
Client GitHubClientSpec `json:"client"`
}
// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider.
// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App.
//
// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured
// as OIDCClients.
//
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps
// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host`
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:subresource:status
type GitHubIdentityProvider struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// Spec for configuring the identity provider.
Spec GitHubIdentityProviderSpec `json:"spec"`
// Status of the identity provider.
Status GitHubIdentityProviderStatus `json:"status,omitempty"`
}
// GitHubIdentityProviderList lists GitHubIdentityProvider objects.
//
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type GitHubIdentityProviderList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []GitHubIdentityProvider `json:"items"`
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -32,7 +32,7 @@ type LDAPIdentityProviderStatus struct {
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}
type LDAPIdentityProviderBind struct {
@@ -101,31 +101,20 @@ type LDAPIdentityProviderGroupSearch struct {
// Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g.
// "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and
// authenticated users will not belong to any groups from the LDAP provider. Also, when not specified,
// the values of Filter, UserAttributeForFilter, Attributes, and SkipGroupRefresh are ignored.
// 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
// value of an attribute of the user entry found as a result of the user search. Which attribute's
// value is used to replace the placeholder(s) depends on the value of UserAttributeForFilter.
// For more information about LDAP filters, see https://ldap.com/ldap-filters.
// 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"`
// UserAttributeForFilter specifies which attribute's value from the user entry found as a result of
// the user search will be used to replace the "{}" placeholder(s) in the group search Filter.
// For example, specifying "uid" as the UserAttributeForFilter while specifying
// "&(objectClass=posixGroup)(memberUid={})" as the Filter would search for groups by replacing
// the "{}" placeholder in the Filter with the value of the user's "uid" attribute.
// Optional. When not specified, the default will act as if "dn" were specified. For example, leaving
// UserAttributeForFilter unspecified while specifying "&(objectClass=groupOfNames)(member={})" as the Filter
// would search for groups by replacing the "{}" placeholder(s) with the dn (distinguished name) of the user.
// +optional
UserAttributeForFilter string `json:"userAttributeForFilter,omitempty"`
// Attributes specifies how the group's information should be read from each LDAP entry which was found as
// the result of the group search.
// +optional

View File

@@ -0,0 +1,75 @@
// Copyright 2020-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"`
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -32,7 +32,7 @@ type OIDCIdentityProviderStatus struct {
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
@@ -138,17 +138,6 @@ type OIDCClaims struct {
// the ID token.
// +optional
Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
}
// OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@@ -1,47 +1,11 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
// CertificateAuthorityDataSourceKind enumerates the sources for CA Bundles.
//
// +kubebuilder:validation:Enum=Secret;ConfigMap
type CertificateAuthorityDataSourceKind string
const (
// CertificateAuthorityDataSourceKindConfigMap uses a Kubernetes configmap to source CA Bundles.
CertificateAuthorityDataSourceKindConfigMap = CertificateAuthorityDataSourceKind("ConfigMap")
// CertificateAuthorityDataSourceKindSecret uses a Kubernetes secret to source CA Bundles.
// Secrets used to source CA Bundles must be of type kubernetes.io/tls or Opaque.
CertificateAuthorityDataSourceKindSecret = CertificateAuthorityDataSourceKind("Secret")
)
// CertificateAuthorityDataSourceSpec provides a source for CA bundle used for client-side TLS verification.
type CertificateAuthorityDataSourceSpec struct {
// Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
// Allowed values are "Secret" or "ConfigMap".
// "ConfigMap" uses a Kubernetes configmap to source CA Bundles.
// "Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
Kind CertificateAuthorityDataSourceKind `json:"kind"`
// Name is the resource name of the secret or configmap from which to read the CA bundle.
// The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// Key is the key name within the secret or configmap from which to read the CA bundle.
// The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
// certificate bundle.
// +kubebuilder:validation:MinLength=1
Key string `json:"key"`
}
// TLSSpec provides TLS configuration for identity provider integration.
// Configuration for TLS parameters related to identity provider integration.
type TLSSpec struct {
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
// +optional
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
// Reference to a CA bundle in a secret or a configmap.
// Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
// +optional
CertificateAuthorityDataSource *CertificateAuthorityDataSourceSpec `json:"certificateAuthorityDataSource,omitempty"`
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -15,7 +15,6 @@ const (
IDPTypeOIDC IDPType = "oidc"
IDPTypeLDAP IDPType = "ldap"
IDPTypeActiveDirectory IDPType = "activedirectory"
IDPTypeGitHub IDPType = "github"
IDPFlowCLIPassword IDPFlow = "cli_password"
IDPFlowBrowserAuthcode IDPFlow = "browser_authcode"
@@ -55,8 +54,7 @@ type OIDCDiscoveryResponseIDPEndpoint struct {
// IDPDiscoveryResponse is the response of a FederationDomain's identity provider discovery endpoint.
type IDPDiscoveryResponse struct {
PinnipedIDPs []PinnipedIDP `json:"pinniped_identity_providers"`
PinnipedSupportedIDPTypes []PinnipedSupportedIDPType `json:"pinniped_supported_identity_provider_types"`
PinnipedIDPs []PinnipedIDP `json:"pinniped_identity_providers"`
}
// PinnipedIDP describes a single identity provider as included in the response of a FederationDomain's
@@ -66,8 +64,3 @@ type PinnipedIDP struct {
Type IDPType `json:"type"`
Flows []IDPFlow `json:"flows,omitempty"`
}
// PinnipedSupportedIDPType describes a single identity provider type.
type PinnipedSupportedIDPType struct {
Type IDPType `json:"type"`
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package oidc
@@ -15,76 +15,11 @@ const (
// 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 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 is the name of the HTTP request parameter which can be used to help select which
// identity provider should be used for authentication by sending the type of the desired identity provider.
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
IDTokenClaimIssuer = "iss"
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
IDTokenClaimSubject = "sub"
// IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim
// in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated.
IDTokenSubClaimIDPNameQueryParam = "idpName"
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
IDTokenClaimAuthorizedParty = "azp"
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
// username which was mapped from the upstream identity provider.
IDTokenClaimUsername = "username"
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
// group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code"
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
GrantTypeRefreshToken = "refresh_token"
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
ScopeOpenID = "openid"
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
// tokens.
ScopeOfflineAccess = "offline_access"
// ScopeEmail is name of the email scope defined by the OIDC spec.
ScopeEmail = "email"
// ScopeProfile is name of the profile scope defined by the OIDC spec.
ScopeProfile = "profile"
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
// ID tokens.
ScopeUsername = "username"
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
// ID tokens.
ScopeGroups = "groups"
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
// be used to request a different audience.
ScopeRequestAudience = "pinniped:request-audience"
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
ClientIDPinnipedCLI = "pinniped-cli"
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
)

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2021 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.
@@ -8,31 +8,14 @@ import (
"encoding/base64"
"encoding/json"
"io"
"io/ioutil"
"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.
//nolint: gochecknoglobals // these are swapped during unit tests.
var (
getenv = os.Getenv
fail = log.Fatalf
@@ -49,11 +32,11 @@ func main() {
case "sleep":
sleep(math.MaxInt64)
case "print":
certBytes, err := os.ReadFile(getenv("CERT_PATH"))
certBytes, err := ioutil.ReadFile(getenv("CERT_PATH"))
if err != nil {
fail("could not read CERT_PATH: %v", err)
}
keyBytes, err := os.ReadFile(getenv("KEY_PATH"))
keyBytes, err := ioutil.ReadFile(getenv("KEY_PATH"))
if err != nil {
fail("could not read KEY_PATH: %v", err)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package main
@@ -90,11 +90,12 @@ func TestEntrypoint(t *testing.T) {
}`,
},
} {
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 ...any) {
fail = func(format string, v ...interface{}) {
testLog.Printf(format, v...)
panic(exited)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package main is the combined entrypoint for all Pinniped server components.
@@ -8,23 +8,20 @@
package main
import (
"fmt"
"os"
"path/filepath"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog/v2"
concierge "go.pinniped.dev/internal/concierge/server"
// this side effect import ensures that we use fipsonly crypto in fips_strict mode.
_ "go.pinniped.dev/internal/crypto/ptls"
lua "go.pinniped.dev/internal/localuserauthenticator"
"go.pinniped.dev/internal/plog"
supervisor "go.pinniped.dev/internal/supervisor/server"
)
//nolint:gochecknoglobals // these are swapped during unit tests.
//nolint: gochecknoglobals // these are swapped during unit tests.
var (
fail = plog.Fatal
fail = klog.Fatalf
subcommands = map[string]func(){
"pinniped-concierge": concierge.Main,
"pinniped-supervisor": supervisor.Main,
@@ -34,11 +31,11 @@ var (
func main() {
if len(os.Args) == 0 {
fail(fmt.Errorf("missing os.Args"))
fail("missing os.Args")
}
binary := filepath.Base(os.Args[0])
if subcommands[binary] == nil {
fail(fmt.Errorf("must be invoked as one of %v, not %q", sets.StringKeySet(subcommands).List(), binary))
fail("must be invoked as one of %v, not %q", sets.StringKeySet(subcommands).List(), binary)
}
subcommands[binary]()
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package main
@@ -38,15 +38,13 @@ func TestEntrypoint(t *testing.T) {
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 ...any) {
testLog.Print(err)
if len(keysAndValues) > 0 {
testLog.Print(keysAndValues...)
}
fail = func(format string, v ...interface{}) {
testLog.Printf(format, v...)
panic(exited)
}

22
cmd/pinniped/cmd/alpha.go Normal file
View File

@@ -0,0 +1,22 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"github.com/spf13/cobra"
)
//nolint: gochecknoglobals
var alphaCmd = &cobra.Command{
Use: "alpha",
Short: "alpha",
Long: "alpha subcommands (syntax or flags are still subject to change)",
SilenceUsage: true, // do not print usage message when commands fail
Hidden: true,
}
//nolint: gochecknoinits
func init() {
rootCmd.AddCommand(alphaCmd)
}

View File

@@ -1,46 +0,0 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"net/http"
"go.pinniped.dev/internal/httputil/roundtripper"
"go.pinniped.dev/internal/plog"
)
type auditIDLoggerFunc func(path string, statusCode int, auditID string)
func logAuditID(path string, statusCode int, auditID string) {
plog.Info("Received auditID for failed request",
"path", path,
"statusCode", statusCode,
"auditID", auditID)
}
func LogAuditIDTransportWrapper(rt http.RoundTripper) http.RoundTripper {
return logAuditIDTransportWrapper(rt, logAuditID)
}
func logAuditIDTransportWrapper(rt http.RoundTripper, auditIDLoggerFunc auditIDLoggerFunc) http.RoundTripper {
return roundtripper.WrapFunc(rt, func(r *http.Request) (*http.Response, error) {
response, responseErr := rt.RoundTrip(r)
if responseErr != nil ||
response == nil ||
response.Header.Get("audit-ID") == "" ||
response.Request == nil ||
response.Request.URL == nil {
return response, responseErr
}
// Use the request path from the response's request, in case the
// original request was modified by any other roudtrippers in the chain.
auditIDLoggerFunc(response.Request.URL.Path,
response.StatusCode,
response.Header.Get("audit-ID"))
return response, responseErr
})
}

View File

@@ -1,116 +0,0 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"errors"
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/require"
"go.pinniped.dev/internal/httputil/roundtripper"
)
func TestLogAuditIDTransportWrapper(t *testing.T) {
canonicalAuditIdHeaderName := "Audit-Id"
tests := []struct {
name string
response *http.Response
responseErr error
want func(t *testing.T, called func()) auditIDLoggerFunc
wantCalled bool
}{
{
name: "happy HTTP response - no error and no log",
response: &http.Response{ // no headers
StatusCode: http.StatusOK,
Request: &http.Request{
URL: &url.URL{
Path: "some-path-from-response-request",
},
},
},
responseErr: nil,
want: func(t *testing.T, called func()) auditIDLoggerFunc {
return func(_ string, _ int, _ string) {
called()
}
},
wantCalled: false, // make it obvious
},
{
name: "nil HTTP response - no error and no log",
response: nil,
responseErr: nil,
want: func(t *testing.T, called func()) auditIDLoggerFunc {
return func(_ string, _ int, _ string) {
called()
}
},
wantCalled: false, // make it obvious
},
{
name: "err HTTP response - no error and no log",
response: nil,
responseErr: errors.New("some error"),
want: func(t *testing.T, called func()) auditIDLoggerFunc {
return func(_ string, _ int, _ string) {
called()
}
},
wantCalled: false, // make it obvious
},
{
name: "happy HTTP response with audit-ID - logs",
response: &http.Response{
Header: http.Header{
canonicalAuditIdHeaderName: []string{"some-audit-id", "some-other-audit-id-that-will-never-be-seen"},
},
StatusCode: http.StatusBadGateway, // statusCode does not matter
Request: &http.Request{
URL: &url.URL{
Path: "some-path-from-response-request",
},
},
},
want: func(t *testing.T, called func()) auditIDLoggerFunc {
return func(path string, statusCode int, auditID string) {
called()
require.Equal(t, "some-path-from-response-request", path)
require.Equal(t, http.StatusBadGateway, statusCode)
require.Equal(t, "some-audit-id", auditID)
}
},
wantCalled: true, // make it obvious
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.NotNil(t, test.want)
mockRequest := &http.Request{
URL: &url.URL{
Path: "should-never-use-this-path",
},
}
var mockRt roundtripper.Func = func(r *http.Request) (*http.Response, error) {
require.Equal(t, mockRequest, r)
return test.response, test.responseErr
}
called := false
subjectRt := logAuditIDTransportWrapper(mockRt, test.want(t, func() {
called = true
}))
actualResponse, err := subjectRt.RoundTrip(mockRequest) //nolint:bodyclose // there is no Body.
require.Equal(t, test.responseErr, err) // This roundtripper only returns mocked errors.
require.Equal(t, test.response, actualResponse)
require.Equal(t, test.wantCalled, called,
"want logFunc to be called: %t, actually was called: %t", test.wantCalled, called)
})
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -8,12 +8,12 @@ import (
"crypto/x509"
"flag"
"fmt"
"os"
"io/ioutil"
"strings"
"github.com/spf13/pflag"
conciergeconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
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.
@@ -62,12 +62,12 @@ func (f *conciergeModeFlag) Type() string {
}
// MatchesFrontend returns true iff the flag matches the type of the provided frontend.
func (f *conciergeModeFlag) MatchesFrontend(frontend *conciergeconfigv1alpha1.CredentialIssuerFrontend) bool {
func (f *conciergeModeFlag) MatchesFrontend(frontend *configv1alpha1.CredentialIssuerFrontend) bool {
switch *f {
case modeImpersonationProxy:
return frontend.Type == conciergeconfigv1alpha1.ImpersonationProxyFrontendType
return frontend.Type == configv1alpha1.ImpersonationProxyFrontendType
case modeTokenCredentialRequestAPI:
return frontend.Type == conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType
return frontend.Type == configv1alpha1.TokenCredentialRequestAPIFrontendType
case modeUnknown:
fallthrough
default:
@@ -85,7 +85,7 @@ func (f *caBundleFlag) String() string {
}
func (f *caBundleFlag) Set(path string) error {
pem, err := os.ReadFile(path)
pem, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("could not read CA bundle path: %w", err)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -6,15 +6,16 @@ package cmd
import (
"bytes"
"fmt"
"os"
"io/ioutil"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
conciergeconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
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) {
@@ -24,14 +25,14 @@ func TestConciergeModeFlag(t *testing.T) {
require.NoError(t, f.Set(""))
require.Equal(t, modeUnknown, f)
require.EqualError(t, f.Set("foo"), `invalid mode "foo", valid modes are TokenCredentialRequestAPI and ImpersonationProxy`)
require.True(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType}))
require.True(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.ImpersonationProxyFrontendType}))
require.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(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType}))
require.False(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.ImpersonationProxyFrontendType}))
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)
@@ -40,8 +41,8 @@ func TestConciergeModeFlag(t *testing.T) {
require.NoError(t, f.Set("ImpersonationProxy"))
require.Equal(t, modeImpersonationProxy, f)
require.Equal(t, "ImpersonationProxy", f.String())
require.False(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType}))
require.True(t, f.MatchesFrontend(&conciergeconfigv1alpha1.CredentialIssuerFrontend{Type: conciergeconfigv1alpha1.ImpersonationProxyFrontendType}))
require.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)
@@ -51,12 +52,12 @@ func TestConciergeModeFlag(t *testing.T) {
func TestCABundleFlag(t *testing.T) {
testCA, err := certauthority.New("Test CA", 1*time.Hour)
require.NoError(t, err)
tmpdir := t.TempDir()
tmpdir := testutil.TempDir(t)
emptyFilePath := filepath.Join(tmpdir, "empty")
require.NoError(t, os.WriteFile(emptyFilePath, []byte{}, 0600))
require.NoError(t, ioutil.WriteFile(emptyFilePath, []byte{}, 0600))
testCAPath := filepath.Join(tmpdir, "testca.pem")
require.NoError(t, os.WriteFile(testCAPath, testCA.Bundle(), 0600))
require.NoError(t, ioutil.WriteFile(testCAPath, testCA.Bundle(), 0600))
f := caBundleFlag{}
require.Equal(t, "path", f.Type())

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -14,7 +14,7 @@ import (
"github.com/spf13/cobra/doc"
)
//nolint:gochecknoinits
//nolint: gochecknoinits
func init() {
rootCmd.AddCommand(generateMarkdownHelpCommand())
}
@@ -24,7 +24,7 @@ func generateMarkdownHelpCommand() *cobra.Command {
Args: cobra.NoArgs,
Use: "generate-markdown-help",
Short: "Generate markdown help for the current set of non-hidden CLI commands",
SilenceUsage: true, // do not print usage message when commands fail
SilenceUsage: true,
Hidden: true,
RunE: runGenerateMarkdownHelp,
}

View File

@@ -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 cmd
@@ -7,14 +7,10 @@ import (
"github.com/spf13/cobra"
)
//nolint:gochecknoglobals
var getCmd = &cobra.Command{
Use: "get",
Short: "Gets one of [kubeconfig]",
SilenceUsage: true, // Do not print usage message when commands fail.
}
//nolint: gochecknoglobals
var getCmd = &cobra.Command{Use: "get", Short: "get"}
//nolint:gochecknoinits
//nolint: gochecknoinits
func init() {
rootCmd.AddCommand(getCmd)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -10,14 +10,17 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-logr/logr"
"github.com/go-logr/stdr"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
@@ -25,33 +28,29 @@ import (
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
conciergeconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/plog"
)
type kubeconfigDeps struct {
getenv func(key string) string
getPathToSelf func() (string, error)
getClientset getConciergeClientsetFunc
log plog.MinLogger
log logr.Logger
}
func kubeconfigRealDeps() kubeconfigDeps {
return kubeconfigDeps{
getenv: os.Getenv,
getPathToSelf: os.Executable,
getClientset: getRealConciergeClientset,
log: plog.New(),
log: stdr.New(log.New(os.Stderr, "", 0)),
}
}
//nolint:gochecknoinits
//nolint: gochecknoinits
func init() {
getCmd.AddCommand(kubeconfigCommand(kubeconfigRealDeps()))
}
@@ -98,12 +97,6 @@ type getKubeconfigParams struct {
credentialCachePath string
credentialCachePathSet bool
installHint string
pinnipedCliPath string
}
type discoveryResponseScopesSupported struct {
// Same as ScopesSupported in the Supervisor's discovery handler's struct.
ScopesSupported []string `json:"scopes_supported"`
}
func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
@@ -112,7 +105,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
Args: cobra.NoArgs,
Use: "kubeconfig",
Short: "Generate a Pinniped-based kubeconfig for a cluster",
SilenceUsage: true, // do not print usage message when commands fail
SilenceUsage: true,
}
flags getKubeconfigParams
namespace string // unused now
@@ -135,9 +128,9 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
f.Var(&flags.concierge.mode, "concierge-mode", "Concierge mode of operation")
f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)")
f.StringVar(&flags.oidc.clientID, "oidc-client-id", oidcapi.ClientIDPinnipedCLI, "OpenID Connect client ID (default: autodiscover)")
f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)")
f.Uint16Var(&flags.oidc.listenPort, "oidc-listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups}, "OpenID Connect scopes to request during login")
f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OpenID Connect scopes to request during login")
f.BoolVar(&flags.oidc.skipBrowser, "oidc-skip-browser", false, "During OpenID Connect login, skip opening the browser (just print the URL)")
f.BoolVar(&flags.oidc.skipListen, "oidc-skip-listen", false, "During OpenID Connect login, skip starting a localhost callback listener (manual copy/paste flow only)")
f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file")
@@ -145,38 +138,25 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache")
f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
f.StringVar(&flags.oidc.upstreamIDPName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor")
f.StringVar(
&flags.oidc.upstreamIDPType,
"upstream-identity-provider-type",
"",
fmt.Sprintf(
"The type of the upstream identity provider used during login with a Supervisor (e.g. '%s', '%s', '%s', '%s')",
idpdiscoveryv1alpha1.IDPTypeOIDC,
idpdiscoveryv1alpha1.IDPTypeLDAP,
idpdiscoveryv1alpha1.IDPTypeActiveDirectory,
idpdiscoveryv1alpha1.IDPTypeGitHub,
),
)
f.StringVar(&flags.oidc.upstreamIDPType, "upstream-identity-provider-type", "", 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))
f.StringVar(&flags.oidc.upstreamIDPFlow, "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.IDPFlowCLIPassword, idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode))
f.StringVar(&flags.kubeconfigPath, "kubeconfig", deps.getenv("KUBECONFIG"), "Path to kubeconfig file")
f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
f.BoolVar(&flags.skipValidate, "skip-validation", false, "Skip final validation of the kubeconfig (default: false)")
f.DurationVar(&flags.timeout, "timeout", 10*time.Minute, "Timeout for autodiscovery and validation")
f.StringVarP(&flags.outputPath, "output", "o", "", "Output file path (default: stdout)")
f.StringVar(&flags.generatedNameSuffix, "generated-name-suffix", "-pinniped", "Suffix to append to generated cluster, context, user kubeconfig entries")
f.StringVar(&flags.credentialCachePath, "credential-cache", "", "Path to cluster-specific credentials cache")
f.StringVar(&flags.pinnipedCliPath, "pinniped-cli-path", "", "Full path or executable name for the Pinniped CLI binary to be embedded in the resulting kubeconfig output (e.g. 'pinniped') (default: full path of the binary used to execute this command)")
f.StringVar(&flags.installHint, "install-hint", "The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli for more details", "This text is shown to the user when the pinniped CLI is not installed.")
mustMarkHidden(cmd, "oidc-debug-session-cache")
mustMarkHidden(cmd,
"oidc-debug-session-cache",
"oidc-skip-listen", // --oidc-skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case.
"concierge-namespace",
)
// --oidc-skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case.
mustMarkHidden(cmd, "oidc-skip-listen")
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
mustMarkHidden(cmd, "concierge-namespace")
cmd.RunE = func(cmd *cobra.Command, _args []string) error {
cmd.RunE = func(cmd *cobra.Command, args []string) error {
if flags.outputPath != "" {
out, err := os.Create(flags.outputPath)
if err != nil {
@@ -191,15 +171,11 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
return cmd
}
//nolint:funlen
func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, flags getKubeconfigParams) error {
ctx, cancel := context.WithTimeout(ctx, flags.timeout)
defer cancel()
// the log statements in this file assume that Info logs are unconditionally printed, so we set the global level to info
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelInfo, Format: plog.FormatCLI}); err != nil {
return err
}
// Validate api group suffix and immediately return an error if it is invalid.
if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil {
return fmt.Errorf("invalid API group suffix: %w", err)
@@ -254,9 +230,11 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f
cluster.CertificateAuthorityData = flags.concierge.caBundle
}
if len(flags.oidc.issuer) > 0 {
err = pinnipedSupervisorDiscovery(ctx, &flags, deps.log)
if err != nil {
// If there is an issuer, and if any upstream IDP flags are not already set, then try to discover Supervisor upstream IDP details.
// When all the upstream IDP flags are set by the user, then skip discovery and don't validate their input. Maybe they know something
// that we can't know, like the name of an IDP that they are going to define in the future.
if len(flags.oidc.issuer) > 0 && (flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "" || flags.oidc.upstreamIDPFlow == "") {
if err := discoverSupervisorUpstreamIDP(ctx, &flags); err != nil {
return err
}
}
@@ -284,12 +262,7 @@ func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdap
execConfig.InstallHint = flags.installHint
var err error
execConfig.Command, err = func() (string, error) {
if flags.pinnipedCliPath != "" {
return flags.pinnipedCliPath, nil
}
return deps.getPathToSelf()
}()
execConfig.Command, err = deps.getPathToSelf()
if err != nil {
return nil, fmt.Errorf("could not determine the Pinniped executable path: %w", err)
}
@@ -316,7 +289,7 @@ func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdap
if flags.staticToken != "" && flags.staticTokenEnvName != "" {
return nil, fmt.Errorf("only one of --static-token and --static-token-env can be specified")
}
execConfig.Args = slices.Concat([]string{"login", "static"}, execConfig.Args)
execConfig.Args = append([]string{"login", "static"}, execConfig.Args...)
if flags.staticToken != "" {
execConfig.Args = append(execConfig.Args, "--token="+flags.staticToken)
}
@@ -327,7 +300,7 @@ func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdap
}
// Otherwise continue to parse the OIDC-related flags and output a config that runs `pinniped login oidc`.
execConfig.Args = slices.Concat([]string{"login", "oidc"}, execConfig.Args)
execConfig.Args = append([]string{"login", "oidc"}, execConfig.Args...)
if flags.oidc.issuer == "" {
return nil, fmt.Errorf("could not autodiscover --oidc-issuer and none was provided")
}
@@ -355,9 +328,6 @@ func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdap
execConfig.Args = append(execConfig.Args, "--debug-session-cache")
}
if flags.oidc.requestAudience != "" {
if strings.Contains(flags.oidc.requestAudience, ".pinniped.dev") {
return nil, fmt.Errorf("request audience is not allowed to include the substring '.pinniped.dev': %s", flags.oidc.requestAudience)
}
execConfig.Args = append(execConfig.Args, "--request-audience="+flags.oidc.requestAudience)
}
if flags.oidc.upstreamIDPName != "" {
@@ -393,7 +363,7 @@ func getCurrentContext(currentKubeConfig clientcmdapi.Config, flags getKubeconfi
return &kubeconfigNames{ContextName: contextName, UserName: ctx.AuthInfo, ClusterName: ctx.Cluster}, nil
}
func waitForCredentialIssuer(ctx context.Context, clientset conciergeclientset.Interface, flags getKubeconfigParams, deps kubeconfigDeps) (*conciergeconfigv1alpha1.CredentialIssuer, error) {
func waitForCredentialIssuer(ctx context.Context, clientset conciergeclientset.Interface, flags getKubeconfigParams, deps kubeconfigDeps) (*configv1alpha1.CredentialIssuer, error) {
credentialIssuer, err := lookupCredentialIssuer(clientset, flags.concierge.credentialIssuer, deps.log)
if err != nil {
return nil, err
@@ -429,7 +399,7 @@ func waitForCredentialIssuer(ctx context.Context, clientset conciergeclientset.I
return credentialIssuer, nil
}
func discoverConciergeParams(credentialIssuer *conciergeconfigv1alpha1.CredentialIssuer, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, log plog.MinLogger) error {
func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, log logr.Logger) error {
// Autodiscover the --concierge-mode.
frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode)
if err != nil {
@@ -440,10 +410,10 @@ func discoverConciergeParams(credentialIssuer *conciergeconfigv1alpha1.Credentia
// Auto-set --concierge-mode if it wasn't explicitly set.
if flags.concierge.mode == modeUnknown {
switch frontend.Type {
case conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType:
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
log.Info("discovered Concierge operating in TokenCredentialRequest API mode")
flags.concierge.mode = modeTokenCredentialRequestAPI
case conciergeconfigv1alpha1.ImpersonationProxyFrontendType:
case configv1alpha1.ImpersonationProxyFrontendType:
log.Info("discovered Concierge operating in impersonation proxy mode")
flags.concierge.mode = modeImpersonationProxy
}
@@ -452,9 +422,9 @@ func discoverConciergeParams(credentialIssuer *conciergeconfigv1alpha1.Credentia
// Auto-set --concierge-endpoint if it wasn't explicitly set.
if flags.concierge.endpoint == "" {
switch frontend.Type {
case conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType:
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
flags.concierge.endpoint = v1Cluster.Server
case conciergeconfigv1alpha1.ImpersonationProxyFrontendType:
case configv1alpha1.ImpersonationProxyFrontendType:
flags.concierge.endpoint = frontend.ImpersonationProxyInfo.Endpoint
}
log.Info("discovered Concierge endpoint", "endpoint", flags.concierge.endpoint)
@@ -463,9 +433,9 @@ func discoverConciergeParams(credentialIssuer *conciergeconfigv1alpha1.Credentia
// Auto-set --concierge-ca-bundle if it wasn't explicitly set..
if len(flags.concierge.caBundle) == 0 {
switch frontend.Type {
case conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType:
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
flags.concierge.caBundle = v1Cluster.CertificateAuthorityData
case conciergeconfigv1alpha1.ImpersonationProxyFrontendType:
case configv1alpha1.ImpersonationProxyFrontendType:
data, err := base64.StdEncoding.DecodeString(frontend.ImpersonationProxyInfo.CertificateAuthorityData)
if err != nil {
return fmt.Errorf("autodiscovered Concierge CA bundle is invalid: %w", err)
@@ -477,7 +447,7 @@ func discoverConciergeParams(credentialIssuer *conciergeconfigv1alpha1.Credentia
return nil
}
func logStrategies(credentialIssuer *conciergeconfigv1alpha1.CredentialIssuer, log plog.MinLogger) {
func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log logr.Logger) {
for _, strategy := range credentialIssuer.Status.Strategies {
log.Info("found CredentialIssuer strategy",
"type", strategy.Type,
@@ -488,9 +458,9 @@ func logStrategies(credentialIssuer *conciergeconfigv1alpha1.CredentialIssuer, l
}
}
func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log plog.MinLogger) error {
func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log logr.Logger) error {
switch auth := authenticator.(type) {
case *authenticationv1alpha1.WebhookAuthenticator:
case *conciergev1alpha1.WebhookAuthenticator:
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
// them to point at the discovered WebhookAuthenticator.
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
@@ -498,7 +468,7 @@ func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconf
flags.concierge.authenticatorType = "webhook"
flags.concierge.authenticatorName = auth.Name
}
case *authenticationv1alpha1.JWTAuthenticator:
case *conciergev1alpha1.JWTAuthenticator:
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
// them to point at the discovered JWTAuthenticator.
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
@@ -533,19 +503,19 @@ func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconf
return nil
}
func getConciergeFrontend(credentialIssuer *conciergeconfigv1alpha1.CredentialIssuer, mode conciergeModeFlag) (*conciergeconfigv1alpha1.CredentialIssuerFrontend, error) {
func getConciergeFrontend(credentialIssuer *configv1alpha1.CredentialIssuer, mode conciergeModeFlag) (*configv1alpha1.CredentialIssuerFrontend, error) {
for _, strategy := range credentialIssuer.Status.Strategies {
// Skip unhealthy strategies.
if strategy.Status != conciergeconfigv1alpha1.SuccessStrategyStatus {
if strategy.Status != configv1alpha1.SuccessStrategyStatus {
continue
}
// Backfill the .status.strategies[].frontend field from .status.kubeConfigInfo for backwards compatibility.
if strategy.Type == conciergeconfigv1alpha1.KubeClusterSigningCertificateStrategyType && strategy.Frontend == nil && credentialIssuer.Status.KubeConfigInfo != nil {
if strategy.Type == configv1alpha1.KubeClusterSigningCertificateStrategyType && strategy.Frontend == nil && credentialIssuer.Status.KubeConfigInfo != nil {
strategy = *strategy.DeepCopy()
strategy.Frontend = &conciergeconfigv1alpha1.CredentialIssuerFrontend{
Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType,
TokenCredentialRequestAPIInfo: &conciergeconfigv1alpha1.TokenCredentialRequestAPIInfo{
strategy.Frontend = &configv1alpha1.CredentialIssuerFrontend{
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
Server: credentialIssuer.Status.KubeConfigInfo.Server,
CertificateAuthorityData: credentialIssuer.Status.KubeConfigInfo.CertificateAuthorityData,
},
@@ -559,7 +529,7 @@ func getConciergeFrontend(credentialIssuer *conciergeconfigv1alpha1.CredentialIs
// Skip any unknown frontend types.
switch strategy.Frontend.Type {
case conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType, conciergeconfigv1alpha1.ImpersonationProxyFrontendType:
case configv1alpha1.TokenCredentialRequestAPIFrontendType, configv1alpha1.ImpersonationProxyFrontendType:
default:
continue
}
@@ -587,7 +557,7 @@ func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.E
}
}
func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log plog.MinLogger) (*conciergeconfigv1alpha1.CredentialIssuer, error) {
func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log logr.Logger) (*configv1alpha1.CredentialIssuer, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc()
@@ -613,7 +583,7 @@ func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string,
return result, nil
}
func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log plog.MinLogger) (metav1.Object, error) {
func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log logr.Logger) (metav1.Object, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc()
@@ -674,7 +644,7 @@ func writeConfigAsYAML(out io.Writer, config clientcmdapi.Config) error {
return nil
}
func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconfig clientcmdapi.Config, log plog.MinLogger) error {
func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconfig clientcmdapi.Config, log logr.Logger) error {
if flags.skipValidate {
return nil
}
@@ -737,7 +707,7 @@ func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconf
log.Info("validated connection to the cluster", "attempts", attempts)
return nil
}
log.Info("could not connect to cluster, retrying...", "error", err, "attempts", attempts, "remaining", time.Until(deadline).Round(time.Second).String())
log.Error(err, "could not connect to cluster, retrying...", "attempts", attempts, "remaining", time.Until(deadline).Round(time.Second).String())
}
}
}
@@ -745,87 +715,33 @@ func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconf
func countCACerts(pemData []byte) int {
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pemData)
return len(pool.Subjects()) //nolint:staticcheck // there's no other clear way to mimic this legacy behavior
return len(pool.Subjects())
}
func hasPendingStrategy(credentialIssuer *conciergeconfigv1alpha1.CredentialIssuer) bool {
func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool {
for _, strategy := range credentialIssuer.Status.Strategies {
if strategy.Reason == conciergeconfigv1alpha1.PendingStrategyReason {
if strategy.Reason == configv1alpha1.PendingStrategyReason {
return true
}
}
return false
}
func pinnipedSupervisorDiscovery(ctx context.Context, flags *getKubeconfigParams, log plog.MinLogger) error {
// Make a client suitable for calling the provider, which may or may not be a Pinniped Supervisor.
oidcProviderHTTPClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle)
func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams) error {
httpClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle)
if err != nil {
return err
}
// Call the provider's discovery endpoint, but don't parse the results yet.
discoveredProvider, err := discoverOIDCProvider(ctx, flags.oidc.issuer, oidcProviderHTTPClient)
if err != nil {
return err
}
// Parse the discovery response to find the Supervisor IDP discovery endpoint.
pinnipedIDPsEndpoint, err := discoverIDPsDiscoveryEndpointURL(discoveredProvider)
pinnipedIDPsEndpoint, err := discoverIDPsDiscoveryEndpointURL(ctx, flags.oidc.issuer, httpClient)
if err != nil {
return err
}
if pinnipedIDPsEndpoint == "" {
// The issuer is not advertising itself as a Pinniped Supervisor which supports upstream IDP discovery.
// Since this field is not present, then assume that the provider is not a Pinniped Supervisor. This field
// was added to the discovery response in v0.9.0, which is so long ago that we can assume there are no such
// old Supervisors in the wild which need to work with this CLI command anymore. Since the issuer is not a
// Supervisor, then there is no need to do the rest of the Supervisor-specific business logic below related
// to username/groups scopes or IDP types/names/flows.
return nil
}
// Now that we know that the provider is a Supervisor, perform an additional check based on its response.
// The username and groups scopes were added to the Supervisor in v0.20.0, and were also added to the
// "scopes_supported" field in the discovery response in that same version. If this CLI command is talking
// to an older Supervisor, then remove the username and groups scopes from the list of requested scopes
// since they will certainly cause an error from the old Supervisor during authentication.
supervisorSupportsBothUsernameAndGroupsScopes, err := discoverScopesSupportedIncludesBothUsernameAndGroups(discoveredProvider)
if err != nil {
return err
}
if !supervisorSupportsBothUsernameAndGroupsScopes {
flags.oidc.scopes = slices.DeleteFunc(flags.oidc.scopes, func(scope string) bool {
if scope == oidcapi.ScopeUsername || scope == oidcapi.ScopeGroups {
log.Info("removed scope from --oidc-scopes list because it is not supported by this Supervisor", "scope", scope)
return true // Remove username and groups scopes if there were present in the flags.
}
return false // Keep any other scopes in the flag list.
})
}
// If any upstream IDP flags are not already set, then try to discover Supervisor upstream IDP details.
// When all the upstream IDP flags are set by the user, then skip discovery and don't validate their input.
// Maybe they know something that we can't know, like the name of an IDP that they are going to define in the
// future.
if flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "" || flags.oidc.upstreamIDPFlow == "" {
if err := discoverSupervisorUpstreamIDP(ctx, pinnipedIDPsEndpoint, oidcProviderHTTPClient, flags, log); err != nil {
return err
}
}
return nil
}
func discoverOIDCProvider(ctx context.Context, issuer string, httpClient *http.Client) (*coreosoidc.Provider, error) {
discoveredProvider, err := coreosoidc.NewProvider(coreosoidc.ClientContext(ctx, httpClient), issuer)
if err != nil {
return nil, fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
}
return discoveredProvider, nil
}
func discoverSupervisorUpstreamIDP(ctx context.Context, pinnipedIDPsEndpoint string, httpClient *http.Client, flags *getKubeconfigParams, log plog.MinLogger) error {
discoveredUpstreamIDPs, err := discoverAllAvailableSupervisorUpstreamIDPs(ctx, pinnipedIDPsEndpoint, httpClient)
if err != nil {
return err
@@ -843,7 +759,7 @@ func discoverSupervisorUpstreamIDP(ctx context.Context, pinnipedIDPsEndpoint str
return err
}
selectedIDPFlow, err := selectUpstreamIDPFlow(discoveredIDPFlows, selectedIDPName, selectedIDPType, flags.oidc.upstreamIDPFlow, log)
selectedIDPFlow, err := selectUpstreamIDPFlow(discoveredIDPFlows, selectedIDPName, selectedIDPType, flags.oidc.upstreamIDPFlow)
if err != nil {
return err
}
@@ -865,22 +781,19 @@ func newDiscoveryHTTPClient(caBundleFlag caBundleFlag) (*http.Client, error) {
return phttp.Default(rootCAs), nil
}
func discoverIDPsDiscoveryEndpointURL(discoveredProvider *coreosoidc.Provider) (string, error) {
var body idpdiscoveryv1alpha1.OIDCDiscoveryResponse
err := discoveredProvider.Claims(&body)
func discoverIDPsDiscoveryEndpointURL(ctx context.Context, issuer string, httpClient *http.Client) (string, error) {
discoveredProvider, err := oidc.NewProvider(oidc.ClientContext(ctx, httpClient), issuer)
if err != nil {
return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
}
return body.SupervisorDiscovery.PinnipedIDPsEndpoint, nil
}
func discoverScopesSupportedIncludesBothUsernameAndGroups(discoveredProvider *coreosoidc.Provider) (bool, error) {
var body discoveryResponseScopesSupported
err := discoveredProvider.Claims(&body)
var body idpdiscoveryv1alpha1.OIDCDiscoveryResponse
err = discoveredProvider.Claims(&body)
if err != nil {
return false, fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
}
return slices.Contains(body.ScopesSupported, oidcapi.ScopeUsername) && slices.Contains(body.ScopesSupported, oidcapi.ScopeGroups), nil
return body.SupervisorDiscovery.PinnipedIDPsEndpoint, nil
}
func discoverAllAvailableSupervisorUpstreamIDPs(ctx context.Context, pinnipedIDPsEndpoint string, httpClient *http.Client) ([]idpdiscoveryv1alpha1.PinnipedIDP, error) {
@@ -900,7 +813,7 @@ func discoverAllAvailableSupervisorUpstreamIDPs(ctx context.Context, pinnipedIDP
return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: unexpected http response status: %s", response.Status)
}
rawBody, err := io.ReadAll(response.Body)
rawBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: could not read response body: %w", err)
}
@@ -986,7 +899,7 @@ func selectUpstreamIDPNameAndType(pinnipedIDPs []idpdiscoveryv1alpha1.PinnipedID
}
}
func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, selectedIDPName string, selectedIDPType idpdiscoveryv1alpha1.IDPType, specifiedFlow string, log plog.MinLogger) (idpdiscoveryv1alpha1.IDPFlow, error) {
func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, selectedIDPName string, selectedIDPType idpdiscoveryv1alpha1.IDPType, specifiedFlow string) (idpdiscoveryv1alpha1.IDPFlow, error) {
switch {
case len(discoveredIDPFlows) == 0:
// No flows listed by discovery means that we are talking to an old Supervisor from before this feature existed.
@@ -1010,9 +923,10 @@ func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, se
return discoveredIDPFlows[0], nil
default:
// The user did not specify a flow, and more than one was found.
log.Info("multiple client flows found, selecting first value as default",
"idpName", selectedIDPName, "idpType", selectedIDPType,
"selectedFlow", discoveredIDPFlows[0].String(), "availableFlows", discoveredIDPFlows)
return discoveredIDPFlows[0], nil
return "", fmt.Errorf(
"multiple client flows for Supervisor upstream identity provider %q of type %q were found, "+
"so the --upstream-identity-provider-flow flag must be specified. "+
"Found these flows: %v",
selectedIDPName, selectedIDPType, discoveredIDPFlows)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -7,30 +7,18 @@ import (
"github.com/spf13/cobra"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/client-go/tools/auth/exec"
"go.pinniped.dev/internal/here"
)
//nolint:gochecknoglobals
//nolint: gochecknoglobals
var loginCmd = &cobra.Command{
Use: "login",
Short: "Authenticates with one of [oidc, static]",
Long: here.Doc(
`Authenticates with one of [oidc, static]
Use "pinniped get kubeconfig" to generate a kubeconfig file which will include
one of these login subcommands in its configuration. The oidc and static
subcommands are not meant to be invoked directly by a user.
The oidc and static subcommands are Kubernetes client-go credential plugins
which are meant to be configured inside a kubeconfig file. (See the Kubernetes
authentication documentation for more information about client-go credential
plugins.)`,
),
Use: "login",
Short: "login",
Long: "Login to a Pinniped server",
SilenceUsage: true, // Do not print usage message when commands fail.
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)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -9,20 +9,22 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/coreos/go-oidc/v3/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/here"
"go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/pkg/conciergeclient"
@@ -31,37 +33,15 @@ import (
"go.pinniped.dev/pkg/oidcclient/oidctypes"
)
const (
// The user may override the flow selection made by `--upstream-identity-provider-flow` using an env var.
// This allows the user to override their default flow selected inside their Pinniped-compatible kubeconfig file.
// A user might want to use this env var, for example, to choose the "browser_authcode" flow when using a kubeconfig
// which specifies "cli_password" when using an IDE plugin where there is no interactive CLI available. This allows
// the user to use one kubeconfig file for both flows.
upstreamIdentityProviderFlowEnvVarName = "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW"
// When using a browser-based login flow, the user may skip printing the login URL to the screen in the case
// where the browser was launched with the login URL. This can be useful, for example, when using a console-based
// UI like k9s, to avoid having any output to stderr which may confuse the UI. Set this env var to "true" to
// skip printing the URL.
skipPrintLoginURLEnvVarName = "PINNIPED_SKIP_PRINT_LOGIN_URL"
// Set this env var to "true" to cause debug logs to be printed to stderr.
debugEnvVarName = "PINNIPED_DEBUG"
// The value to use for true/false env vars to enable the behavior caused by the env var.
envVarTruthyValue = "true"
)
//nolint:gochecknoinits
//nolint: gochecknoinits
func init() {
loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps()))
}
type oidcLoginCommandDeps struct {
lookupEnv func(string) (string, bool)
login func(string, string, ...oidcclient.Option) (*oidctypes.Token, error)
exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error)
optionsFactory OIDCClientOptions
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 {
@@ -71,7 +51,6 @@ func oidcLoginCommandRealDeps() oidcLoginCommandDeps {
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
return client.ExchangeToken(ctx, token)
},
optionsFactory: &clientOptions{},
}
}
@@ -102,29 +81,18 @@ type oidcLoginFlags struct {
func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
var (
cmd = &cobra.Command{
Args: cobra.NoArgs,
Use: "oidc --issuer ISSUER",
Short: "Login using an OpenID Connect provider",
Long: here.Doc(
`Login using an OpenID Connect provider
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
login command in its configuration. This login command is not meant to be
invoked directly by a user.
This login command is a Kubernetes client-go credential plugin which is meant to
be configured inside a kubeconfig file. (See the Kubernetes authentication
documentation for more information about client-go credential plugins.)`,
),
SilenceUsage: true, // do not print usage message when commands fail
Args: cobra.NoArgs,
Use: "oidc --issuer ISSUER",
Short: "Login using an OpenID Connect provider",
SilenceUsage: true,
}
flags oidcLoginFlags
conciergeNamespace string // unused now
)
cmd.Flags().StringVar(&flags.issuer, "issuer", "", "OpenID Connect issuer URL")
cmd.Flags().StringVar(&flags.clientID, "client-id", oidcapi.ClientIDPinnipedCLI, "OpenID Connect client ID")
cmd.Flags().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")
@@ -141,23 +109,14 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)")
cmd.Flags().StringVar(&flags.upstreamIdentityProviderName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor")
cmd.Flags().StringVar(&flags.upstreamIdentityProviderType,
"upstream-identity-provider-type",
idpdiscoveryv1alpha1.IDPTypeOIDC.String(),
fmt.Sprintf(
"The type of the upstream identity provider used during login with a Supervisor (e.g. '%s', '%s', '%s', '%s')",
idpdiscoveryv1alpha1.IDPTypeOIDC,
idpdiscoveryv1alpha1.IDPTypeLDAP,
idpdiscoveryv1alpha1.IDPTypeActiveDirectory,
idpdiscoveryv1alpha1.IDPTypeGitHub,
))
cmd.Flags().StringVar(&flags.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))
// --skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case.
mustMarkHidden(cmd, "skip-listen")
mustMarkHidden(cmd, "debug-session-cache")
mustMarkRequired(cmd, "issuer")
cmd.RunE = func(cmd *cobra.Command, _args []string) error { return runOIDCLogin(cmd, deps, flags) }
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")
@@ -166,7 +125,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
}
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { //nolint:funlen
pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv)
pLogger, err := SetLogLevel(deps.lookupEnv, "Pinniped login: ")
if err != nil {
plog.WarningErr("Received error while setting log level", err)
}
@@ -174,47 +133,44 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
// 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 := klogr.New().WithName("session")
sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) {
pLogger.Error("error during session cache operation", err)
logger.Error(err, "error during session cache operation")
}))
}
sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...)
// Initialize the login handler.
opts := []oidcclient.Option{
deps.optionsFactory.WithContext(cmd.Context()),
deps.optionsFactory.WithLoginLogger(pLogger),
deps.optionsFactory.WithScopes(flags.scopes),
deps.optionsFactory.WithSessionCache(sessionCache),
}
skipPrintLoginURL, _ := deps.lookupEnv(skipPrintLoginURLEnvVarName)
if skipPrintLoginURL == envVarTruthyValue {
opts = append(opts, deps.optionsFactory.WithSkipPrintLoginURL())
oidcclient.WithContext(cmd.Context()),
oidcclient.WithLogger(klogr.New()),
oidcclient.WithScopes(flags.scopes),
oidcclient.WithSessionCache(sessionCache),
}
if flags.listenPort != 0 {
opts = append(opts, deps.optionsFactory.WithListenPort(flags.listenPort))
opts = append(opts, oidcclient.WithListenPort(flags.listenPort))
}
if flags.requestAudience != "" {
opts = append(opts, deps.optionsFactory.WithRequestAudience(flags.requestAudience))
opts = append(opts, oidcclient.WithRequestAudience(flags.requestAudience))
}
if flags.upstreamIdentityProviderName != "" {
opts = append(opts, deps.optionsFactory.WithUpstreamIdentityProvider(
opts = append(opts, oidcclient.WithUpstreamIdentityProvider(
flags.upstreamIdentityProviderName, flags.upstreamIdentityProviderType))
}
requestedFlow, flowSource := idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow), "--upstream-identity-provider-flow"
if flowOverride, hasFlowOverride := deps.lookupEnv(upstreamIdentityProviderFlowEnvVarName); hasFlowOverride {
requestedFlow, flowSource = idpdiscoveryv1alpha1.IDPFlow(flowOverride), upstreamIdentityProviderFlowEnvVarName
}
if requestedFlow != "" {
opts = append(opts, deps.optionsFactory.WithLoginFlow(requestedFlow, flowSource))
flowOpts, err := flowOptions(
idpdiscoveryv1alpha1.IDPType(flags.upstreamIdentityProviderType),
idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow),
)
if err != nil {
return err
}
opts = append(opts, flowOpts...)
var concierge *conciergeclient.Client
if flags.conciergeEnabled {
@@ -224,7 +180,6 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
conciergeclient.WithTransportWrapper(LogAuditIDTransportWrapper),
)
if err != nil {
return fmt.Errorf("invalid Concierge parameters: %w", err)
@@ -233,12 +188,12 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
// --skip-browser skips opening the browser.
if flags.skipBrowser {
opts = append(opts, deps.optionsFactory.WithSkipBrowserOpen())
opts = append(opts, oidcclient.WithSkipBrowserOpen())
}
// --skip-listen skips starting the localhost callback listener.
if flags.skipListen {
opts = append(opts, deps.optionsFactory.WithSkipListen())
opts = append(opts, oidcclient.WithSkipListen())
}
if len(flags.caBundlePaths) > 0 || len(flags.caBundleData) > 0 {
@@ -246,7 +201,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
if err != nil {
return err
}
opts = append(opts, deps.optionsFactory.WithClient(client))
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 {
@@ -266,12 +221,12 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
}
pLogger.Debug("Performing OIDC login", "issuer", flags.issuer, "client id", flags.clientID)
// Do the basic login to get an OIDC token. Although this can return several tokens, we only need the ID token here.
// Do the basic login to get an OIDC token.
token, err := deps.login(flags.issuer, flags.clientID, opts...)
if err != nil {
return fmt.Errorf("could not complete Pinniped login: %w", err)
}
cred := tokenCredential(token.IDToken)
cred := tokenCredential(token)
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
if concierge != nil {
@@ -296,10 +251,50 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
}
func flowOptions(requestedIDPType idpdiscoveryv1alpha1.IDPType, requestedFlow idpdiscoveryv1alpha1.IDPFlow) ([]oidcclient.Option, error) {
useCLIFlow := []oidcclient.Option{oidcclient.WithCLISendingCredentials()}
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(
"--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)",
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:
fallthrough // not supported for LDAP providers, so fallthrough to error case
default:
return nil, fmt.Errorf(
"--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)",
requestedIDPType, requestedFlow, []string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.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)
}
@@ -315,41 +310,38 @@ func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, er
return phttp.Default(pool), nil
}
func tokenCredential(idToken *oidctypes.IDToken) *clientauthv1beta1.ExecCredential {
func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
cred := clientauthv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
Kind: "ExecCredential",
APIVersion: "client.authentication.k8s.io/v1beta1",
},
Status: &clientauthv1beta1.ExecCredentialStatus{
Token: idToken.Token,
Token: token.IDToken.Token,
},
}
if !idToken.Expiry.IsZero() {
cred.Status.ExpirationTimestamp = &idToken.Expiry
if !token.IDToken.Expiry.IsZero() {
cred.Status.ExpirationTimestamp = &token.IDToken.Expiry
}
return &cred
}
func SetLogLevel(ctx context.Context, lookupEnv func(string) (string, bool)) (plog.Logger, error) {
debug, _ := lookupEnv(debugEnvVarName)
if debug == envVarTruthyValue {
err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug, Format: plog.FormatCLI})
func SetLogLevel(lookupEnv func(string) (string, bool), prefix string) (plog.Logger, error) {
debug, _ := lookupEnv("PINNIPED_DEBUG")
if debug == "true" {
err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug)
if err != nil {
return nil, err
}
}
return plog.New(), nil
logger := plog.New(prefix)
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"

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -8,24 +8,20 @@ import (
"context"
"encoding/base64"
"fmt"
"os"
"io/ioutil"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
clocktesting "k8s.io/utils/clock/testing"
"k8s.io/klog/v2"
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
"go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/mocks/mockoidcclientoptions"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger"
"go.pinniped.dev/pkg/conciergeclient"
"go.pinniped.dev/pkg/oidcclient"
"go.pinniped.dev/pkg/oidcclient/oidctypes"
@@ -36,23 +32,12 @@ func TestLoginOIDCCommand(t *testing.T) {
testCA, err := certauthority.New("Test CA", 1*time.Hour)
require.NoError(t, err)
tmpdir := t.TempDir()
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)
defaultWantedOptions := func(f *mockoidcclientoptions.MockOIDCClientOptions) {
f.EXPECT().WithContext(gomock.Any())
f.EXPECT().WithLoginLogger(gomock.Any())
f.EXPECT().WithScopes([]string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups})
f.EXPECT().WithSessionCache(gomock.Any())
}
tests := []struct {
name string
args []string
@@ -62,7 +47,6 @@ func TestLoginOIDCCommand(t *testing.T) {
wantError bool
wantStdout string
wantStderr string
wantOptions func(f *mockoidcclientoptions.MockOIDCClientOptions)
wantOptionsCount int
wantLogs []string
}{
@@ -72,14 +56,6 @@ func TestLoginOIDCCommand(t *testing.T) {
wantStdout: here.Doc(`
Login using an OpenID Connect provider
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
login command in its configuration. This login command is not meant to be
invoked directly by a user.
This login command is a Kubernetes client-go credential plugin which is meant to
be configured inside a kubeconfig file. (See the Kubernetes authentication
documentation for more information about client-go credential plugins.)
Usage:
oidc --issuer ISSUER [flags]
@@ -98,12 +74,12 @@ func TestLoginOIDCCommand(t *testing.T) {
--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])
--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)
--upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'browser_authcode', 'cli_password')
--upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor
--upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') (default "oidc")
--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")
`),
},
{
@@ -121,8 +97,7 @@ func TestLoginOIDCCommand(t *testing.T) {
"--issuer", "test-issuer",
"--enable-concierge",
},
wantOptions: defaultWantedOptions,
wantError: true,
wantError: true,
wantStderr: here.Doc(`
Error: invalid Concierge parameters: endpoint must not be empty
`),
@@ -134,8 +109,7 @@ func TestLoginOIDCCommand(t *testing.T) {
"--issuer", "test-issuer",
"--ca-bundle", "./does/not/exist",
},
wantOptions: defaultWantedOptions,
wantError: true,
wantError: true,
wantStderr: here.Doc(`
Error: could not read --ca-bundle: open ./does/not/exist: no such file or directory
`),
@@ -147,8 +121,7 @@ func TestLoginOIDCCommand(t *testing.T) {
"--issuer", "test-issuer",
"--ca-bundle-data", "invalid-base64",
},
wantOptions: defaultWantedOptions,
wantError: true,
wantError: true,
wantStderr: here.Doc(`
Error: could not read --ca-bundle-data: illegal base64 data at input byte 7
`),
@@ -163,12 +136,22 @@ func TestLoginOIDCCommand(t *testing.T) {
"--concierge-authenticator-name", "test-authenticator",
"--concierge-endpoint", "https://127.0.0.1:1234/",
},
wantOptions: defaultWantedOptions,
wantError: true,
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",
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: "oidc upstream type with default flow is allowed",
args: []string{
@@ -177,57 +160,121 @@ func TestLoginOIDCCommand(t *testing.T) {
"--upstream-identity-provider-type", "oidc",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
wantOptions: defaultWantedOptions,
wantOptionsCount: 4,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "PINNIPED_SKIP_PRINT_LOGIN_URL adds an option",
name: "oidc upstream type with CLI 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
},
env: map[string]string{"PINNIPED_SKIP_PRINT_LOGIN_URL": "true"},
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
defaultWantedOptions(f)
f.EXPECT().WithSkipPrintLoginURL()
},
wantOptionsCount: 5,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "--upstream-identity-provider-flow adds an option",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-flow", "cli_password",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
defaultWantedOptions(f)
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlowCLIPassword, "--upstream-identity-provider-flow")
wantOptionsCount: 5,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "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 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: "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: "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW adds an option that overrides --upstream-identity-provider-flow",
name: "activedirectory upstream type with default flow is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-flow", "ignored-value-from-param",
"--upstream-identity-provider-type", "activedirectory",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "actual-value-from-env"},
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
defaultWantedOptions(f)
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlow("actual-value-from-env"), "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW")
},
wantOptionsCount: 5,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "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 unsupported flow is an error",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "ldap",
"--upstream-identity-provider-flow", "browser_authcode", // "browser_authcode" is only supported for OIDC upstreams
"--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": browser_authcode (supported values: [cli_password])
`),
},
{
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 unsupported flow is an error",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "activedirectory",
"--upstream-identity-provider-flow", "browser_authcode", // "browser_authcode" is only supported for OIDC upstreams
"--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": browser_authcode (supported values: [cli_password])
`),
},
{
name: "login error",
args: []string{
@@ -236,7 +283,6 @@ func TestLoginOIDCCommand(t *testing.T) {
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
loginErr: fmt.Errorf("some login error"),
wantOptions: defaultWantedOptions,
wantOptionsCount: 4,
wantError: true,
wantStderr: here.Doc(`
@@ -255,7 +301,6 @@ func TestLoginOIDCCommand(t *testing.T) {
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
conciergeErr: fmt.Errorf("some concierge error"),
wantOptions: defaultWantedOptions,
wantOptionsCount: 4,
wantError: true,
wantStderr: here.Doc(`
@@ -270,12 +315,11 @@ func TestLoginOIDCCommand(t *testing.T) {
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_DEBUG": "true"},
wantOptions: defaultWantedOptions,
wantOptionsCount: 4,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
wantLogs: []string{
nowStr + ` cmd/login_oidc.go:268 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
nowStr + ` cmd/login_oidc.go:288 No concierge configured, skipping token credential exchange`,
"\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"",
"\"level\"=0 \"msg\"=\"Pinniped login: No concierge configured, skipping token credential exchange\"",
},
},
{
@@ -296,49 +340,29 @@ func TestLoginOIDCCommand(t *testing.T) {
"--concierge-endpoint", "https://127.0.0.1:1234/",
"--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()),
"--concierge-api-group-suffix", "some.suffix.com",
"--credential-cache", t.TempDir() + "/credentials.yaml", // must specify --credential-cache or else the cache file on disk causes test pollution
"--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",
"--upstream-identity-provider-flow", "some-flow-type",
},
env: map[string]string{"PINNIPED_DEBUG": "true", "PINNIPED_SKIP_PRINT_LOGIN_URL": "true"},
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
f.EXPECT().WithContext(gomock.Any())
f.EXPECT().WithLoginLogger(gomock.Any())
f.EXPECT().WithScopes([]string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups})
f.EXPECT().WithSessionCache(gomock.Any())
f.EXPECT().WithListenPort(uint16(1234))
f.EXPECT().WithSkipBrowserOpen()
f.EXPECT().WithSkipListen()
f.EXPECT().WithSkipPrintLoginURL()
f.EXPECT().WithClient(gomock.Any())
f.EXPECT().WithRequestAudience("cluster-1234")
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlow("some-flow-type"), "--upstream-identity-provider-flow")
f.EXPECT().WithUpstreamIdentityProvider("some-upstream-name", "ldap")
},
wantOptionsCount: 12,
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 + ` cmd/login_oidc.go:268 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
nowStr + ` cmd/login_oidc.go:278 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
nowStr + ` cmd/login_oidc.go:286 Successfully exchanged token for cluster credential.`,
nowStr + ` cmd/login_oidc.go:293 caching cluster credential for future use.`,
"\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"",
"\"level\"=0 \"msg\"=\"Pinniped login: Exchanging token for cluster credential\" \"authenticator name\"=\"test-authenticator\" \"authenticator type\"=\"webhook\" \"endpoint\"=\"https://127.0.0.1:1234/\"",
"\"level\"=0 \"msg\"=\"Pinniped login: Successfully exchanged token for cluster credential.\"",
"\"level\"=0 \"msg\"=\"Pinniped login: caching cluster credential for future use.\"",
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
ctx := plog.AddZapOverridesToContext(context.Background(), t, &buf, nil, clocktesting.NewFakeClock(now))
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
optionsFactory := mockoidcclientoptions.NewMockOIDCClientOptions(ctrl)
if tt.wantOptions != nil {
tt.wantOptions(optionsFactory)
}
var gotOptions []oidcclient.Option
testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
klog.SetLogger(testLogger.Logger)
var (
gotOptions []oidcclient.Option
)
cmd := oidcLoginCommand(oidcLoginCommandDeps{
lookupEnv: func(s string) (string, bool) {
v, ok := tt.env[s]
@@ -373,7 +397,6 @@ func TestLoginOIDCCommand(t *testing.T) {
},
}, nil
},
optionsFactory: optionsFactory,
})
require.NotNil(t, cmd)
@@ -381,7 +404,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 {
@@ -391,7 +414,7 @@ func TestLoginOIDCCommand(t *testing.T) {
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
require.Len(t, gotOptions, tt.wantOptionsCount)
require.Equal(t, tt.wantLogs, testutil.SplitByNewline(buf.String()))
require.Equal(t, tt.wantLogs, testLogger.Lines())
})
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"time"
@@ -16,13 +17,12 @@ import (
"go.pinniped.dev/internal/execcredcache"
"go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/pkg/conciergeclient"
"go.pinniped.dev/pkg/oidcclient/oidctypes"
)
//nolint:gochecknoinits
//nolint: gochecknoinits
func init() {
loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps()))
}
@@ -56,21 +56,10 @@ type staticLoginParams struct {
func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
var (
cmd = &cobra.Command{
Args: cobra.NoArgs,
Use: "static [--token TOKEN] [--token-env TOKEN_NAME]",
Short: "Login using a static token",
Long: here.Doc(
`Login using a static token
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
login command in its configuration. This login command is not meant to be
invoked directly by a user.
This login command is a Kubernetes client-go credential plugin which is meant to
be configured inside a kubeconfig file. (See the Kubernetes authentication
documentation for more information about client-go credential plugins.)`,
),
SilenceUsage: true, // do not print usage message when commands fail
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
@@ -86,7 +75,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
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) }
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) }
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
mustMarkHidden(cmd, "concierge-namespace")
@@ -94,9 +83,8 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
return cmd
}
func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginParams) error {
out := cmd.OutOrStdout()
pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv)
func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error {
pLogger, err := SetLogLevel(deps.lookupEnv, "Pinniped login: ")
if err != nil {
plog.WarningErr("Received error while setting log level", err)
}
@@ -113,7 +101,6 @@ func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginP
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
conciergeclient.WithTransportWrapper(LogAuditIDTransportWrapper),
)
if err != nil {
return fmt.Errorf("invalid Concierge parameters: %w", err)
@@ -134,7 +121,7 @@ func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginP
return fmt.Errorf("--token-env variable %q is empty", flags.staticTokenEnvName)
}
}
cred := tokenCredential(&oidctypes.IDToken{Token: token})
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 {

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -7,19 +7,21 @@ import (
"bytes"
"context"
"fmt"
"os"
"io/ioutil"
"path/filepath"
"testing"
"time"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/testutil/testlogger"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
clocktesting "k8s.io/utils/clock/testing"
"go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/pkg/conciergeclient"
)
@@ -29,13 +31,9 @@ func TestLoginStaticCommand(t *testing.T) {
testCA, err := certauthority.New("Test CA", 1*time.Hour)
require.NoError(t, err)
tmpdir := t.TempDir()
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
@@ -55,14 +53,6 @@ func TestLoginStaticCommand(t *testing.T) {
wantStdout: here.Doc(`
Login using a static token
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
login command in its configuration. This login command is not meant to be
invoked directly by a user.
This login command is a Kubernetes client-go credential plugin which is meant to
be configured inside a kubeconfig file. (See the Kubernetes authentication
documentation for more information about client-go credential plugins.)
Usage:
static [--token TOKEN] [--token-env TOKEN_NAME] [flags]
@@ -146,9 +136,7 @@ func TestLoginStaticCommand(t *testing.T) {
wantStderr: here.Doc(`
Error: could not complete Concierge credential exchange: some concierge error
`),
wantLogs: []string{
nowStr + ` cmd/login_static.go:160 exchanging static token for cluster credential {"endpoint": "https://127.0.0.1/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
},
wantLogs: []string{"\"level\"=0 \"msg\"=\"Pinniped login: exchanging static token for cluster credential\" \"authenticator name\"=\"test-authenticator\" \"authenticator type\"=\"webhook\" \"endpoint\"=\"https://127.0.0.1/\""},
},
{
name: "invalid API group suffix",
@@ -175,10 +163,10 @@ func TestLoginStaticCommand(t *testing.T) {
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
ctx := plog.AddZapOverridesToContext(context.Background(), t, &buf, nil, clocktesting.NewFakeClock(now))
testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
klog.SetLogger(testLogger.Logger)
cmd := staticLoginCommand(staticLoginDeps{
lookupEnv: func(s string) (string, bool) {
v, ok := tt.env[s]
@@ -206,7 +194,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 {
@@ -215,7 +203,7 @@ 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, testutil.SplitByNewline(buf.String()))
require.Equal(t, tt.wantLogs, testLogger.Lines())
})
}
}

153
cmd/pinniped/cmd/logout.go Normal file
View File

@@ -0,0 +1,153 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
"go.pinniped.dev/internal/plog"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/spf13/cobra"
"go.pinniped.dev/pkg/oidcclient"
"go.pinniped.dev/pkg/oidcclient/filesession"
)
//nolint: gochecknoinits
func init() {
rootCmd.AddCommand(newLogoutCommand())
}
type logoutFlags struct {
kubeconfigPath string
kubeconfigContextOverride string
}
// This implements client side logout-- i.e. deleting the cached tokens and certificates for a user
// without telling the supervisor to forget about the users tokens. From a user experience
// perspective these are identical, but it leaves orphaned tokens lying around that the supervisor
// won't garbage collect for up to 9 hours.
// Fosite supports token revocation requests ala https://tools.ietf.org/html/rfc7009#section-2.1
// with their TokenRevocationHandler, but we would also want to turn around and revoke the upstream
// tokens in the case of OIDC.
// That's something that could be done to improve security and stop storage from getting too
// big.
// It works by parsing the provided kubeconfig to get the arguments to pinniped login oidc,
// grabbing the issuer and the cache paths, then using that issuer to find and delete the entry
// in the session cache.
func newLogoutCommand() *cobra.Command {
cmd := &cobra.Command{
Args: cobra.NoArgs,
Use: "logout",
Short: "Terminate the current user's session.",
}
flags := &logoutFlags{}
cmd.Flags().StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
cmd.Flags().StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
return runLogout(flags)
}
return cmd
}
func runLogout(flags *logoutFlags) error {
pLogger, err := SetLogLevel(os.LookupEnv, "Pinniped logout: ")
if err != nil {
plog.WarningErr("Received error while setting log level", err)
}
clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride)
currentKubeConfig, err := clientConfig.RawConfig()
if err != nil {
return err
}
// start by getting the current context or another context if provided.
contextName := currentKubeConfig.CurrentContext
if len(flags.kubeconfigContextOverride) > 0 {
contextName = flags.kubeconfigContextOverride
}
kubeContext, ok := currentKubeConfig.Contexts[contextName]
if !ok {
return fmt.Errorf("couldn't find current context")
}
// then get the authinfo associated with that context.
authInfo := currentKubeConfig.AuthInfos[kubeContext.AuthInfo]
if authInfo == nil {
return fmt.Errorf("could not find auth info-- are you sure this is a Pinniped kubeconfig?")
}
// get the exec credential out of the authinfo and validate that it takes the shape of a pinniped login command.
exec := authInfo.Exec
if exec == nil {
return fmt.Errorf("could not find exec credential-- are you sure this is a Pinniped kubeconfig?")
}
execArgs := exec.Args
if execArgs == nil {
return fmt.Errorf("could not find exec credential arguments-- are you sure this is a Pinniped kubeconfig?")
}
// parse the arguments in the exec credential (which should be the pinniped login command).
loginCommand := oidcLoginCommand(oidcLoginCommandDeps{})
err = loginCommand.ParseFlags(execArgs)
if err != nil {
return err
}
// Get the issuer flag. If this doesn't exist we have no way to get in to the cache so we have to exit.
issuer := loginCommand.Flag("issuer").Value.String()
if issuer == "" {
return fmt.Errorf("could not find issuer-- are you sure this is a Pinniped kubeconfig?")
}
// Get the session cache. If it doesn't exist just use the default value.
sessionCachePath := loginCommand.Flag("session-cache").Value.String()
if sessionCachePath == "" {
sessionCachePath = filepath.Join(mustGetConfigDir(), "sessions.yaml")
}
// Get the credential cache. If it doesn't exist just use the default value.
credentialCachePath := loginCommand.Flag("credential-cache").Value.String()
if credentialCachePath == "" {
credentialCachePath = filepath.Join(mustGetConfigDir(), "credentials.yaml")
}
// TODO this should probably be a more targeted removal rather than the whole file...
// but that involves figuring out the cache key which is hard.
// Remove the credential cache that stores the users x509 certificates.
err = os.Remove(credentialCachePath)
// a not found error is fine and we should move on and try to delete the
// session cache if possible. Other errors might be a problem.
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
// Remove the cache entry for this issuer.
var sessionOptions []filesession.Option
sessionCache := filesession.New(sessionCachePath, sessionOptions...)
downstreamScopes := []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience"}
sort.Strings(downstreamScopes)
sessionCacheKey := oidcclient.SessionCacheKey{
Issuer: issuer,
ClientID: "pinniped-cli",
Scopes: downstreamScopes,
RedirectURI: (&url.URL{Scheme: "http", Host: "localhost:0", Path: "/callback"}).String(),
}
deleted := sessionCache.DeleteToken(sessionCacheKey)
if deleted {
pLogger.Warning("Successfully logged out of session.")
} else {
// this is likely because you're already logged out, but you might still want to know.
pLogger.Warning("Could not find session to log out of.")
pLogger.Debug("debug info", "issuer", issuer, "session cache path", sessionCachePath, "credential cache path", credentialCachePath)
}
return nil
}

View File

@@ -1,84 +0,0 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"context"
"net/http"
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
"go.pinniped.dev/pkg/oidcclient"
)
// OIDCClientOptions is an interface that wraps the creation of Options for the purpose of making them
// more friendly to unit tests. Because the Option type refers to a private struct type, it is hard
// to create mocks for them in tests of other packages. This provides a seam that can be mocked.
// No need for this interface to include deprecated options (such as WithLogger), since those should never be invoked.
type OIDCClientOptions interface {
WithContext(ctx context.Context) oidcclient.Option
WithLoginLogger(logger oidcclient.Logger) oidcclient.Option
WithListenPort(port uint16) oidcclient.Option
WithSkipBrowserOpen() oidcclient.Option
WithSkipListen() oidcclient.Option
WithSkipPrintLoginURL() oidcclient.Option
WithSessionCache(cache oidcclient.SessionCache) oidcclient.Option
WithClient(httpClient *http.Client) oidcclient.Option
WithScopes(scopes []string) oidcclient.Option
WithRequestAudience(audience string) oidcclient.Option
WithLoginFlow(loginFlow v1alpha1.IDPFlow, flowSource string) oidcclient.Option
WithUpstreamIdentityProvider(upstreamName, upstreamType string) oidcclient.Option
}
// clientOptions implements OIDCClientOptions for production use.
type clientOptions struct{}
var _ OIDCClientOptions = (*clientOptions)(nil)
func (o *clientOptions) WithContext(ctx context.Context) oidcclient.Option {
return oidcclient.WithContext(ctx)
}
func (o *clientOptions) WithLoginLogger(logger oidcclient.Logger) oidcclient.Option {
return oidcclient.WithLoginLogger(logger)
}
func (o *clientOptions) WithListenPort(port uint16) oidcclient.Option {
return oidcclient.WithListenPort(port)
}
func (o *clientOptions) WithSkipBrowserOpen() oidcclient.Option {
return oidcclient.WithSkipBrowserOpen()
}
func (o *clientOptions) WithSkipListen() oidcclient.Option {
return oidcclient.WithSkipListen()
}
func (o *clientOptions) WithSkipPrintLoginURL() oidcclient.Option {
return oidcclient.WithSkipPrintLoginURL()
}
func (o *clientOptions) WithSessionCache(cache oidcclient.SessionCache) oidcclient.Option {
return oidcclient.WithSessionCache(cache)
}
func (o *clientOptions) WithClient(httpClient *http.Client) oidcclient.Option {
return oidcclient.WithClient(httpClient)
}
func (o *clientOptions) WithScopes(scopes []string) oidcclient.Option {
return oidcclient.WithScopes(scopes)
}
func (o *clientOptions) WithRequestAudience(audience string) oidcclient.Option {
return oidcclient.WithRequestAudience(audience)
}
func (o *clientOptions) WithLoginFlow(loginFlow v1alpha1.IDPFlow, flowSource string) oidcclient.Option {
return oidcclient.WithLoginFlow(loginFlow, flowSource)
}
func (o *clientOptions) WithUpstreamIdentityProvider(upstreamName, upstreamType string) oidcclient.Option {
return oidcclient.WithUpstreamIdentityProvider(upstreamName, upstreamType)
}

View File

@@ -1,35 +1,26 @@
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2022 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/here"
"go.pinniped.dev/internal/plog"
)
//nolint:gochecknoglobals
//nolint: gochecknoglobals
var rootCmd = &cobra.Command{
Use: "pinniped",
Long: here.Doc(
`The Pinniped CLI is the client-side binary for use with Pinniped-enabled Kubernetes clusters
Find more information at: https://pinniped.dev`,
),
Use: "pinniped",
Short: "pinniped",
Long: "pinniped is the client-side binary for use with Pinniped-enabled Kubernetes clusters.",
SilenceUsage: true, // do not print usage message when commands fail
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() error {
defer plog.Setup()()
// the context does not matter here because it is unused when CLI formatting is provided
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(context.Background(), plog.LogSpec{Format: plog.FormatCLI}); err != nil {
return err
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
return rootCmd.Execute()
}

View File

@@ -1,61 +1,28 @@
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
"go.pinniped.dev/internal/pversion"
"k8s.io/component-base/version"
)
//nolint:gochecknoinits
//nolint: gochecknoinits
func init() {
rootCmd.AddCommand(newVersionCommand())
}
//nolint:gochecknoglobals
var (
output = new(string)
// getBuildInfo can be overwritten by tests.
getBuildInfo = pversion.Get
)
func newVersionCommand() *cobra.Command {
c := &cobra.Command{
RunE: runner,
return &cobra.Command{
RunE: func(cmd *cobra.Command, _ []string) error {
fmt.Fprintf(cmd.OutOrStdout(), "%#v\n", version.Get())
return nil
},
Args: cobra.NoArgs, // do not accept positional arguments for this command
Use: "version",
Short: "Print the version of this Pinniped CLI",
}
c.Flags().StringVarP(output, "output", "o", "", "one of 'yaml' or 'json'")
return c
}
func runner(cmd *cobra.Command, _ []string) error {
buildVersion := getBuildInfo()
switch {
case output == nil || *output == "":
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", buildVersion.GitVersion)
case *output == "json":
bytes, err := json.MarshalIndent(buildVersion, "", " ")
if err != nil {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", bytes)
case *output == "yaml":
bytes, err := yaml.Marshal(buildVersion)
if err != nil {
return err
}
_, _ = fmt.Fprint(cmd.OutOrStdout(), string(bytes))
default:
return fmt.Errorf("'%s' is not a valid option for output", *output)
}
return nil
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -9,10 +9,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apimachineryversion "k8s.io/apimachinery/pkg/version"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/pversion"
)
var (
@@ -21,8 +19,8 @@ var (
version \[flags\]
Flags:
-h, --help help for version
-o, --output string one of 'yaml' or 'json'
-h, --help help for version
`)
knownGoodHelpRegexpForVersion = here.Doc(`
@@ -32,55 +30,24 @@ var (
version \[flags\]
Flags:
-h, --help help for version
-o, --output string one of 'yaml' or 'json'
-h, --help help for version
`)
jsonRegexp = here.Doc(`{
"major": "\d*",
"minor": "\d*",
"gitVersion": "i am a version for json output",
"gitCommit": ".*",
"gitTreeState": ".*",
"buildDate": ".*",
"goVersion": ".*",
"compiler": ".*",
"platform": ".*/.*"
}`)
yamlRegexp = here.Doc(`buildDate: ".*"
compiler: .*
gitCommit: .*
gitTreeState: .*
gitVersion: i am a version for yaml output
goVersion: .*
major: "\d*"
minor: "\d*"
platform: .*/.*
`)
emptyVersionRegexp = `version.Info{Major:"", Minor:"", GitVersion:".*", GitCommit:".*", GitTreeState:"", BuildDate:".*", GoVersion:".*", Compiler:".*", Platform:".*/.*"}`
)
func TestNewVersionCmd(t *testing.T) {
t.Cleanup(func() {
getBuildInfo = pversion.Get
})
tests := []struct {
name string
args []string
vars string
getBuildInfo func() apimachineryversion.Info
wantError bool
wantStdoutRegexp string
wantStderrRegexp string
}{
{
name: "no flags",
args: []string{},
getBuildInfo: func() apimachineryversion.Info {
return apimachineryversion.Info{GitVersion: "v55.66.44"}
},
wantStdoutRegexp: "v55.66.44\n",
name: "no flags",
args: []string{},
wantStdoutRegexp: emptyVersionRegexp + "\n",
},
{
name: "help flag passed",
@@ -94,43 +61,10 @@ func TestNewVersionCmd(t *testing.T) {
wantStderrRegexp: `Error: unknown command "tuna" for "version"`,
wantStdoutRegexp: knownGoodUsageRegexpForVersion,
},
{
name: "json output",
args: []string{"--output", "json"},
getBuildInfo: func() apimachineryversion.Info {
return apimachineryversion.Info{
GitVersion: "i am a version for json output",
Platform: "a/b",
}
},
wantStdoutRegexp: jsonRegexp,
},
{
name: "yaml output",
args: []string{"--output", "yaml"},
getBuildInfo: func() apimachineryversion.Info {
return apimachineryversion.Info{
GitVersion: "i am a version for yaml output",
Platform: "c/d",
}
},
wantStdoutRegexp: yamlRegexp,
},
{
name: "incorrect output",
args: []string{"--output", "foo"},
wantError: true,
wantStderrRegexp: `Error: 'foo' is not a valid option for output`,
wantStdoutRegexp: knownGoodUsageRegexpForVersion,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
if tt.getBuildInfo != nil {
getBuildInfo = tt.getBuildInfo
}
cmd := newVersionCommand()
require.NotNil(t, cmd)

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
@@ -12,7 +12,7 @@ import (
"time"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
@@ -24,26 +24,13 @@ import (
"go.pinniped.dev/internal/here"
)
type whoamiDeps struct {
getenv func(key string) string
getClientset getConciergeClientsetFunc
}
func whoamiRealDeps() whoamiDeps {
return whoamiDeps{
getenv: os.Getenv,
getClientset: getRealConciergeClientset,
}
}
//nolint:gochecknoinits
//nolint: gochecknoinits
func init() {
rootCmd.AddCommand(newWhoamiCommand(whoamiRealDeps()))
rootCmd.AddCommand(newWhoamiCommand(getRealConciergeClientset))
}
type whoamiFlags struct {
outputFormat string // e.g., yaml, json, text
timeout time.Duration
kubeconfigPath string
kubeconfigContextOverride string
@@ -56,33 +43,32 @@ type clusterInfo struct {
url string
}
func newWhoamiCommand(deps whoamiDeps) *cobra.Command {
func newWhoamiCommand(getClientset getConciergeClientsetFunc) *cobra.Command {
cmd := &cobra.Command{
Args: cobra.NoArgs, // do not accept positional arguments for this command
Use: "whoami",
Short: "Print information about the current user",
SilenceUsage: true, // do not print usage message when commands fail
SilenceUsage: true,
}
flags := &whoamiFlags{}
// flags
f := cmd.Flags()
f.StringVarP(&flags.outputFormat, "output", "o", "text", "Output format (e.g., 'yaml', 'json', 'text')")
f.StringVar(&flags.kubeconfigPath, "kubeconfig", deps.getenv("KUBECONFIG"), "Path to kubeconfig file")
f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
f.StringVar(&flags.apiGroupSuffix, "api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
f.DurationVar(&flags.timeout, "timeout", 0, "Timeout for the WhoAmI API request (default: 0, meaning no timeout)")
cmd.RunE = func(cmd *cobra.Command, _ []string) error {
return runWhoami(cmd.OutOrStdout(), deps, flags)
return runWhoami(cmd.OutOrStdout(), getClientset, flags)
}
return cmd
}
func runWhoami(output io.Writer, deps whoamiDeps, flags *whoamiFlags) error {
func runWhoami(output io.Writer, getClientset getConciergeClientsetFunc, flags *whoamiFlags) error {
clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride)
clientset, err := deps.getClientset(clientConfig, flags.apiGroupSuffix)
clientset, err := getClientset(clientConfig, flags.apiGroupSuffix)
if err != nil {
return fmt.Errorf("could not configure Kubernetes client: %w", err)
}
@@ -92,26 +78,12 @@ func runWhoami(output io.Writer, deps whoamiDeps, flags *whoamiFlags) error {
return fmt.Errorf("could not get current cluster info: %w", err)
}
// Making the WhoAmI request may cause client-go to invoke the credential plugin, which may
// ask the user to interactively authenticate. The time that the user takes to authenticate
// is included in the timeout time, but their authentication is not cancelled by exceeding
// this timeout. Only the subsequent whoami API request is cancelled. Using a short timeout
// causes the odd behavior of a successful login immediately followed by a whoami request failure
// due to timeout. For comparison, kubectl uses an infinite timeout by default on API requests
// but also allows the user to adjust this timeout with the `--request-timeout` CLI option,
// so we will take a similar approach. Note that kubectl has the same behavior when a client-go
// credential plugin is invoked and the user takes longer then the timeout to authenticate.
ctx := context.Background()
if flags.timeout > 0 {
var cancelFunc context.CancelFunc
ctx, cancelFunc = context.WithTimeout(ctx, flags.timeout)
defer cancelFunc()
}
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc()
whoAmI, err := clientset.IdentityV1alpha1().WhoAmIRequests().Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
if err != nil {
hint := ""
if apierrors.IsNotFound(err) {
if errors.IsNotFound(err) {
hint = " (is the Pinniped WhoAmI API running and healthy?)"
}
return fmt.Errorf("could not complete WhoAmIRequest%s: %w", hint, err)

View File

@@ -1,46 +1,29 @@
// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
kubetesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/clientcmd"
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
conciergefake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
fakeconciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/here"
)
func TestWhoami(t *testing.T) {
helpOutputFormatString := here.Doc(`
Print information about the current user
Usage:
whoami [flags]
Flags:
--api-group-suffix string Concierge API group suffix (default "pinniped.dev")
-h, --help help for whoami
--kubeconfig string Path to kubeconfig file%s
--kubeconfig-context string Kubeconfig context name (default: current active context)
-o, --output string Output format (e.g., 'yaml', 'json', 'text') (default "text")
--timeout duration Timeout for the WhoAmI API request (default: 0, meaning no timeout)
`)
tests := []struct {
name string
args []string
env map[string]string
groupsOverride []string
gettingClientsetErr error
callingAPIErr error
@@ -48,17 +31,21 @@ func TestWhoami(t *testing.T) {
wantStdout, wantStderr string
}{
{
name: "help flag passed",
args: []string{"--help"},
wantStdout: fmt.Sprintf(helpOutputFormatString, ""),
},
{
name: "help flag passed with KUBECONFIG env var set",
env: map[string]string{
"KUBECONFIG": "/path/to/kubeconfig",
},
args: []string{"--help"},
wantStdout: fmt.Sprintf(helpOutputFormatString, ` (default "/path/to/kubeconfig")`),
name: "help flag",
args: []string{"--help"},
wantStdout: here.Doc(`
Print information about the current user
Usage:
whoami [flags]
Flags:
--api-group-suffix string Concierge API group suffix (default "pinniped.dev")
-h, --help help for whoami
--kubeconfig string Path to kubeconfig file
--kubeconfig-context string Kubeconfig context name (default: current active context)
-o, --output string Output format (e.g., 'yaml', 'json', 'text') (default "text")
`),
},
{
name: "text output",
@@ -119,7 +106,8 @@ func TestWhoami(t *testing.T) {
Current user info:
Username: some-username
Groups:` + " \n"), // Linters and codeformatters don't like the extra space after "Groups:" and before the newline
Groups:
`),
},
{
name: "json output",
@@ -285,18 +273,19 @@ func TestWhoami(t *testing.T) {
},
{
name: "calling API fails because WhoAmI API is not installed",
callingAPIErr: apierrors.NewNotFound(identityv1alpha1.SchemeGroupVersion.WithResource("whoamirequests").GroupResource(), "whatever"),
callingAPIErr: errors.NewNotFound(identityv1alpha1.SchemeGroupVersion.WithResource("whoamirequests").GroupResource(), "whatever"),
wantError: true,
wantStderr: "Error: could not complete WhoAmIRequest (is the Pinniped WhoAmI API running and healthy?): whoamirequests.identity.concierge.pinniped.dev \"whatever\" not found\n",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
getClientset := func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) {
if test.gettingClientsetErr != nil {
return nil, test.gettingClientsetErr
}
clientset := conciergefake.NewSimpleClientset()
clientset := fakeconciergeclientset.NewSimpleClientset()
clientset.PrependReactor("create", "whoamirequests", func(_ kubetesting.Action) (bool, runtime.Object, error) {
if test.callingAPIErr != nil {
return true, nil, test.callingAPIErr
@@ -318,22 +307,12 @@ func TestWhoami(t *testing.T) {
})
return clientset, nil
}
cmd := newWhoamiCommand(whoamiDeps{
getenv: func(key string) string {
return test.env[key]
},
getClientset: getClientset,
})
cmd := newWhoamiCommand(getClientset)
stdout, stderr := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd.SetOut(stdout)
cmd.SetErr(stderr)
if test.args == nil {
// cobra uses os.Args[1:] when SetArgs is called with nil, so avoid using nil for tests.
cmd.SetArgs([]string{})
} else {
cmd.SetArgs(test.args)
}
cmd.SetArgs(test.args)
err := cmd.Execute()
if test.wantError {

View File

@@ -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 main
@@ -9,11 +9,9 @@ import (
"github.com/pkg/browser"
"go.pinniped.dev/cmd/pinniped/cmd"
// this side effect import ensures that we use fipsonly crypto in fips_strict mode.
_ "go.pinniped.dev/internal/crypto/ptls"
)
//nolint:gochecknoinits
//nolint: gochecknoinits
func init() {
// browsers like chrome like to write to our std out which breaks our JSON ExecCredential output
// thus we redirect the browser's std out to our std err
@@ -21,7 +19,5 @@ func init() {
}
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
cmd.Execute()
}

View File

@@ -1,9 +1,11 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: jwtauthenticators.authentication.concierge.pinniped.dev
spec:
group: authentication.concierge.pinniped.dev
@@ -25,35 +27,26 @@ spec:
- jsonPath: .spec.audience
name: Audience
type: string
- jsonPath: .status.phase
name: Status
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
JWTAuthenticator describes the configuration of a JWT authenticator.
Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
signature, existence of claims, etc.) and extract the username and groups from the token.
description: "JWTAuthenticator describes the configuration of a JWT authenticator.
\n Upon receiving a signed JWT, a JWTAuthenticator will performs some validation
on it (e.g., valid signature, existence of claims, etc.) and extract the
username and groups from the token."
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@@ -65,25 +58,24 @@ spec:
minLength: 1
type: string
claims:
description: |-
Claims allows customization of the claims that will be mapped to user identity
for Kubernetes access.
description: Claims allows customization of the claims that will be
mapped to user identity for Kubernetes access.
properties:
groups:
description: |-
Groups is the name of the claim which should be read to extract the user's
group membership from the JWT token. When not specified, it will default to "groups".
description: Groups is the name of the claim which should be read
to extract the user's group membership from the JWT token. When
not specified, it will default to "groups".
type: string
username:
description: |-
Username is the name of the claim which should be read to extract the
username from the JWT token. When not specified, it will default to "username".
description: Username is the name of the claim which should be
read to extract the username from the JWT token. When not specified,
it will default to "username".
type: string
type: object
issuer:
description: |-
Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is
also used to validate the "iss" JWT claim.
description: Issuer is the OIDC issuer URL that will be used to discover
public signing keys. Issuer is also used to validate the "iss" JWT
claim.
minLength: 1
pattern: ^https://
type: string
@@ -94,39 +86,6 @@ spec:
description: X.509 Certificate Authority (base64-encoded PEM bundle).
If omitted, a default set of system roots will be trusted.
type: string
certificateAuthorityDataSource:
description: |-
Reference to a CA bundle in a secret or a configmap.
Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
properties:
key:
description: |-
Key is the key name within the secret or configmap from which to read the CA bundle.
The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
certificate bundle.
minLength: 1
type: string
kind:
description: |-
Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
Allowed values are "Secret" or "ConfigMap".
"ConfigMap" uses a Kubernetes configmap to source CA Bundles.
"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
enum:
- Secret
- ConfigMap
type: string
name:
description: |-
Name is the resource name of the secret or configmap from which to read the CA bundle.
The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed.
minLength: 1
type: string
required:
- key
- kind
- name
type: object
type: object
required:
- audience
@@ -139,35 +98,37 @@ spec:
description: Represents the observations of the authenticator's current
state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: 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.
properties:
lastTransitionTime:
description: |-
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.
description: 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.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
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.
description: 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.
format: int64
minimum: 0
type: integer
reason:
description: |-
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.
description: 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.
maxLength: 1024
minLength: 1
@@ -182,6 +143,10 @@ spec:
type: string
type:
description: 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)
maxLength: 316
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])$
type: string
@@ -196,14 +161,6 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
@@ -212,3 +169,9 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,9 +1,11 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: webhookauthenticators.authentication.concierge.pinniped.dev
spec:
group: authentication.concierge.pinniped.dev
@@ -22,9 +24,6 @@ spec:
- jsonPath: .spec.endpoint
name: Endpoint
type: string
- jsonPath: .status.phase
name: Status
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
@@ -35,19 +34,14 @@ spec:
authenticator.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@@ -66,39 +60,6 @@ spec:
description: X.509 Certificate Authority (base64-encoded PEM bundle).
If omitted, a default set of system roots will be trusted.
type: string
certificateAuthorityDataSource:
description: |-
Reference to a CA bundle in a secret or a configmap.
Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
properties:
key:
description: |-
Key is the key name within the secret or configmap from which to read the CA bundle.
The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
certificate bundle.
minLength: 1
type: string
kind:
description: |-
Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
Allowed values are "Secret" or "ConfigMap".
"ConfigMap" uses a Kubernetes configmap to source CA Bundles.
"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
enum:
- Secret
- ConfigMap
type: string
name:
description: |-
Name is the resource name of the secret or configmap from which to read the CA bundle.
The referenced secret or configmap must be created in the same namespace where Pinniped Concierge is installed.
minLength: 1
type: string
required:
- key
- kind
- name
type: object
type: object
required:
- endpoint
@@ -110,35 +71,37 @@ spec:
description: Represents the observations of the authenticator's current
state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: 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.
properties:
lastTransitionTime:
description: |-
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.
description: 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.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
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.
description: 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.
format: int64
minimum: 0
type: integer
reason:
description: |-
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.
description: 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.
maxLength: 1024
minLength: 1
@@ -153,6 +116,10 @@ spec:
type: string
type:
description: 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)
maxLength: 316
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])$
type: string
@@ -167,14 +134,6 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the WebhookAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
@@ -183,3 +142,9 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,9 +1,11 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: credentialissuers.config.concierge.pinniped.dev
spec:
group: config.concierge.pinniped.dev
@@ -33,19 +35,14 @@ spec:
Pinniped Concierge credential issuer.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@@ -57,18 +54,18 @@ spec:
of the Concierge impersonation proxy.
properties:
externalEndpoint:
description: |-
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".
description: "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. \n This field must be non-empty when spec.impersonationProxy.service.type
is \"None\"."
type: string
mode:
description: |-
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.
description: '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.'
enum:
- auto
- enabled
@@ -87,43 +84,26 @@ spec:
pairs to set as annotations on the provisioned Service.
type: object
loadBalancerIP:
description: |-
LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service.
description: LoadBalancerIP specifies the IP address to set
in the spec.loadBalancerIP field of the provisioned Service.
This is not supported on all cloud providers.
maxLength: 255
minLength: 1
type: string
type:
default: LoadBalancer
description: |-
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.
description: "Type specifies the type of Service to provision
for the impersonation proxy. \n 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."
enum:
- LoadBalancer
- ClusterIP
- None
type: string
type: object
tls:
description: |-
TLS contains information about how the Concierge impersonation proxy should serve TLS.
If this field is empty, the impersonation proxy will generate its own TLS certificate.
properties:
certificateAuthorityData:
description: |-
X.509 Certificate Authority (base64-encoded PEM bundle).
Used to advertise the CA bundle for the impersonation proxy endpoint.
type: string
secretName:
description: |-
SecretName is the name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains
the TLS serving certificate for the Concierge impersonation proxy endpoint.
minLength: 1
type: string
type: object
required:
- mode
- service
@@ -135,9 +115,9 @@ spec:
description: CredentialIssuerStatus describes the status of the Concierge.
properties:
kubeConfigInfo:
description: |-
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.
description: 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.
properties:
certificateAuthorityData:
description: The K8s API server CA bundle.
@@ -164,9 +144,9 @@ spec:
this strategy.
properties:
impersonationProxyInfo:
description: |-
ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
This field is only set when Type is "ImpersonationProxy".
description: ImpersonationProxyInfo describes the parameters
for the impersonation proxy on this Concierge. This field
is only set when Type is "ImpersonationProxy".
properties:
certificateAuthorityData:
description: CertificateAuthorityData is the base64-encoded
@@ -184,9 +164,9 @@ spec:
- endpoint
type: object
tokenCredentialRequestInfo:
description: |-
TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
This field is only set when Type is "TokenCredentialRequestAPI".
description: TokenCredentialRequestAPIInfo describes the
parameters for the TokenCredentialRequest API on this
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
properties:
certificateAuthorityData:
description: CertificateAuthorityData is the base64-encoded
@@ -259,3 +239,9 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,4 +1,4 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -12,14 +12,7 @@ apiVersion: v1
kind: Namespace
metadata:
name: #@ data.values.namespace
labels:
_: #@ template.replace(labels())
#! When deploying onto a cluster which has PSAs enabled by default for namespaces,
#! effectively disable them for this namespace. The kube-cert-agent Deployment's pod
#! created by the Concierge in this namespace needs to be able to perform privileged
#! actions. The regular Concierge pod containers created by the Deployment below do
#! not need special privileges and are marked as such in their securityContext settings.
pod-security.kubernetes.io/enforce: privileged
labels: #@ labels()
#@ end
---
apiVersion: v1
@@ -45,9 +38,8 @@ metadata:
annotations:
#! we need to create this service account before we create the secret
kapp.k14s.io/change-group: "impersonation-proxy.concierge.pinniped.dev/serviceaccount"
kubernetes.io/enforce-mountable-secrets: "true"
secrets: [] #! make sure the token controller does not create any secrets
automountServiceAccountToken: false
secrets: #! make sure the token controller does not create any other secrets
- name: #@ defaultResourceNameWithSuffix("impersonation-proxy")
---
apiVersion: v1
kind: ConfigMap
@@ -78,8 +70,6 @@ data:
impersonationCACertificateSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-ca-certificate") @)
impersonationSignerSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-signer-ca-certificate") @)
agentServiceAccount: (@= defaultResourceNameWithSuffix("kube-cert-agent") @)
impersonationProxyServiceAccount: (@= defaultResourceNameWithSuffix("impersonation-proxy") @)
impersonationProxyLegacySecret: (@= defaultResourceNameWithSuffix("impersonation-proxy") @)
labels: (@= json.encode(labels()).rstrip() @)
kubeCertAgent:
namePrefix: (@= defaultResourceNameWithSuffix("kube-cert-agent-") @)
@@ -97,14 +87,8 @@ data:
- image-pull-secret
(@ end @)
(@ if data.values.log_level: @)
log:
level: (@= getAndValidateLogLevel() @)
logLevel: (@= getAndValidateLogLevel() @)
(@ end @)
tls:
onedottwo:
allowedCiphers: (@= str(data.values.allowed_ciphers_for_tls_onedottwo) @)
audit:
logUsernamesAndGroups: (@= data.values.audit.log_usernames_and_groups @)
---
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
apiVersion: v1
@@ -137,6 +121,8 @@ spec:
#! More recently added the more unique deploymentPodLabel() so Services can select these Pods more specifically
#! without accidentally selecting any other Deployment's Pods, especially the kube cert agent Deployment's Pods.
_: #@ template.replace(deploymentPodLabel())
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ""
spec:
securityContext:
runAsUser: #@ data.values.run_as_user
@@ -156,15 +142,6 @@ spec:
imagePullPolicy: IfNotPresent
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: [ "ALL" ]
#! seccompProfile was introduced in Kube v1.19. Using it on an older Kube version will result in a
#! kubectl validation error when installing via `kubectl apply`, which can be ignored using kubectl's
#! `--validate=false` flag. Note that installing via `kapp` does not complain about this validation error.
seccompProfile:
type: "RuntimeDefault"
resources:
requests:
cpu: "100m"
@@ -185,6 +162,9 @@ spec:
- name: podinfo
mountPath: /etc/podinfo
readOnly: true
- name: impersonation-proxy
mountPath: /var/run/secrets/impersonation-proxy.concierge.pinniped.dev/serviceaccount
readOnly: true
env:
#@ if data.values.https_proxy:
- name: HTTPS_PROXY
@@ -220,6 +200,12 @@ spec:
- name: config-volume
configMap:
name: #@ defaultResourceNameWithSuffix("config")
- name: impersonation-proxy
secret:
secretName: #@ defaultResourceNameWithSuffix("impersonation-proxy")
items: #! make sure our pod does not start until the token controller has a chance to populate the secret
- key: token
path: token
- name: podinfo
downwardAPI:
items:
@@ -239,14 +225,9 @@ spec:
effect: NoSchedule
- key: node-role.kubernetes.io/control-plane #! The new name for these nodes as of Kubernetes 1.24.
effect: NoSchedule
- key: kubernetes.io/arch
effect: NoSchedule
operator: Equal
value: amd64 #! Allow running on amd64 nodes.
- key: kubernetes.io/arch
effect: NoSchedule
operator: Equal
value: arm64 #! Also allow running on arm64 nodes.
#! "system-cluster-critical" cannot be used outside the kube-system namespace until Kubernetes >= 1.17,
#! so we skip setting this for now (see https://github.com/kubernetes/kubernetes/issues/60596).
#!priorityClassName: system-cluster-critical
#! This will help make sure our multiple pods run on different nodes, making
#! our deployment "more" "HA".
affinity:
@@ -341,9 +322,17 @@ spec:
#@ if data.values.impersonation_proxy_spec.service.load_balancer_ip:
loadBalancerIP: #@ data.values.impersonation_proxy_spec.service.load_balancer_ip
#@ end
#@ if data.values.impersonation_proxy_spec.service.annotations == None:
annotations:
service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "4000"
#@ else:
annotations: #@ data.values.impersonation_proxy_spec.service.annotations
#@ end
---
apiVersion: v1
kind: Secret
metadata:
name: #@ defaultResourceNameWithSuffix("impersonation-proxy")
namespace: #@ namespace()
labels: #@ labels()
annotations:
#! wait until the SA exists to create this secret so that the token controller does not delete it
#! we have this secret at the end so that kubectl will create the service account first
kapp.k14s.io/change-rule: "upsert after upserting impersonation-proxy.concierge.pinniped.dev/serviceaccount"
kubernetes.io/service-account.name: #@ defaultResourceNameWithSuffix("impersonation-proxy")
type: kubernetes.io/service-account-token

View File

@@ -1,4 +1,4 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -19,7 +19,7 @@ rules:
resources: [ apiservices ]
verbs: [ get, list, patch, update, watch ]
- apiGroups: [ admissionregistration.k8s.io ]
resources: [ validatingwebhookconfigurations, mutatingwebhookconfigurations, validatingadmissionpolicies, validatingadmissionpolicybindings ]
resources: [ validatingwebhookconfigurations, mutatingwebhookconfigurations ]
verbs: [ get, list, watch ]
- apiGroups: [ flowcontrol.apiserver.k8s.io ]
resources: [ flowschemas, prioritylevelconfigurations ]
@@ -43,10 +43,6 @@ rules:
- #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
resources: [ jwtauthenticators, webhookauthenticators ]
verbs: [ get, list, watch ]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
resources: [ jwtauthenticators/status, webhookauthenticators/status ]
verbs: [ get, list, watch, update ]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
@@ -160,13 +156,6 @@ rules:
- apiGroups: [ coordination.k8s.io ]
resources: [ leases ]
verbs: [ create, get, update ]
#! We need to be able to get service accounts and create serviceaccounts/tokens so that we can create short-lived tokens for the impersonation proxy
- apiGroups: [""]
resources: [ serviceaccounts ]
verbs: [ get ]
- apiGroups: [""]
resources: [ serviceaccounts/token ]
verbs: [ create ]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1

View File

@@ -1,245 +1,103 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ def validate_strings_map(obj):
#@ # Returns True if obj is an associative data structure string→string, and False otherwise.
#@ for key in obj:
#@ if type(key) != "string" or type(obj[key]) != "string":
#@ return False
#@ end
#@ end
#@ return True
#@ end
#@data/values-schema
#@data/values
---
#@schema/title "App name"
#@schema/desc "Used to help determine the names of various resources and labels."
#@schema/validation min_len=1
app_name: pinniped-concierge
#@schema/title "Namespace"
#@schema/desc "Creates a new namespace statically in yaml with the given name and installs the app into that namespace."
#@schema/validation min_len=1
#! Creates a new namespace statically in yaml with the given name and installs the app into that namespace.
namespace: pinniped-concierge
#! If specified, assumes that a namespace of the given name already exists and installs the app into that namespace.
#! If both `namespace` and `into_namespace` are specified, then only `into_namespace` is used.
into_namespace: #! e.g. my-preexisting-namespace
#@schema/title "Into namespace"
#@ into_namespace_desc = "If specified, assumes that a namespace of the given name already exists and installs the app into that namespace. \
#@ If both `namespace` and `into_namespace` are specified, then only `into_namespace` is used."
#@schema/desc into_namespace_desc
#@schema/examples ("The name of an existing namespace", "my-preexisting-namespace")
#@schema/nullable
#@schema/validation min_len=1
into_namespace: ""
#! All resources created statically by yaml at install-time and all resources created dynamically
#! by controllers at runtime will be labelled with `app: $app_name` and also with the labels
#! specified here. The value of `custom_labels` must be a map of string keys to string values.
#! The app can be uninstalled either by:
#! 1. Deleting the static install-time yaml resources including the static namespace, which will cascade and also delete
#! resources that were dynamically created by controllers at runtime
#! 2. Or, deleting all resources by label, which does not assume that there was a static install-time yaml namespace.
custom_labels: {} #! e.g. {myCustomLabelName: myCustomLabelValue, otherCustomLabelName: otherCustomLabelValue}
#@schema/title "Custom labels"
#@ custom_labels_desc = "All resources created statically by yaml at install-time and all resources created dynamically \
#@ by controllers at runtime will be labelled with `app: $app_name` and also with the labels specified here. The value of \
#@ `custom_labels` must be a map of string keys to string values. The app can be uninstalled either by: 1.) deleting the \
#@ static install-time yaml resources including the static namespace, which will cascade and also delete \
#@ resources that were dynamically created by controllers at runtime, or 2.) deleting all resources by label, which does \
#@ not assume that there was a static install-time yaml namespace."
#@schema/desc custom_labels_desc
#@schema/examples ("Example set of labels", {"myCustomLabelName": "myCustomLabelValue", "otherCustomLabelName": "otherCustomLabelValue"})
#@schema/type any=True
#@schema/validation ("a map of string keys and string values", validate_strings_map)
custom_labels: { }
#@schema/title "Replicas"
#@schema/desc "Specify how many replicas of the Pinniped server to run."
#! Specify how many replicas of the Pinniped server to run.
replicas: 2
#@schema/title "Image repo"
#@schema/desc "The repository for the Concierge container image."
#@schema/validation min_len=1
image_repo: ghcr.io/vmware-tanzu/pinniped/pinniped-server
#@schema/title "Image digest"
#@schema/desc "The image digest for the Concierge container image. If both image_digest or an image_tag are given, only image_digest will be used."
#@schema/examples ("Providing a digest", "sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8")
#@schema/nullable
#@schema/validation min_len=1, when=lambda _, ctx: ctx.parent["image_tag"] == None
image_digest: ""
#@schema/title "Image tag"
#@schema/desc "The image tag for the Concierge container image. If both image_digest or an image_tag are given, only image_digest will be used."
#@schema/examples ("Providing a tag", "v0.25.0")
#@schema/validation min_len=1, when=lambda _, ctx: ctx.parent["image_digest"] == None
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
image_repo: projects.registry.vmware.com/pinniped/pinniped-server
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
image_tag: latest
#@schema/title "Kube Cert Agent image"
#@ kube_cert_agent_image = "Optionally specify a different image for the 'kube-cert-agent' pod which is scheduled \
#@ on the control plane. This image needs only to include `sleep` and `cat` binaries. \
#@ By default, the same image specified for image_repo/image_digest/image_tag will be re-used."
#@schema/desc kube_cert_agent_image
#@schema/examples ("Image including tag or digest", "ghcr.io/vmware-tanzu/pinniped/pinniped-server:latest")
#@schema/nullable
#@schema/validation min_len=1
kube_cert_agent_image: ""
#! Optionally specify a different image for the "kube-cert-agent" pod which is scheduled
#! on the control plane. This image needs only to include `sleep` and `cat` binaries.
#! By default, the same image specified for image_repo/image_digest/image_tag will be re-used.
kube_cert_agent_image:
#@schema/title "Image pull dockerconfigjson"
#@ image_pull_dockerconfigjson_desc = "A base64 encoded secret to be used when pulling the `image_repo` container image. \
#@ Can be used when the image_repo is a private registry. Typically, the value would be the output of: \
#@ kubectl create secret docker-registry x --docker-server=https://example.io --docker-username='USERNAME' --docker-password='PASSWORD' --dry-run=client -o json | jq -r '.data[\".dockerconfigjson\"]'"
#@schema/desc image_pull_dockerconfigjson_desc
#@ example_desc = 'base64 encoding of: {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}'
#@ example_value = "eyJhdXRocyI6eyJodHRwczovL2V4YW1wbGUuaW8iOnsidXNlcm5hbWUiOiJVU0VSTkFNRSIsInBhc3N3b3JkIjoiUEFTU1dPUkQiLCJhdXRoIjoiVlZORlVrNUJUVVU2VUVGVFUxZFBVa1E9In19fQ=="
#@schema/examples (example_desc, example_value)
#@schema/nullable
#@schema/validation min_len=1
image_pull_dockerconfigjson: ""
#! Specifies a secret to be used when pulling the above `image_repo` container image.
#! Can be used when the above image_repo is a private registry.
#! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]'
#! Optional.
image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}
#@schema/title "Discovery URL"
#@schema/desc "Pinniped will try to guess the right K8s API URL for sharing that information with potential clients. This setting allows the guess to be overridden."
#@schema/examples ("Kubernetes API URL","https://example.com")
#@schema/nullable
#@schema/validation min_len=1
discovery_url: ""
#! Pinniped will try to guess the right K8s API URL for sharing that information with potential clients.
#! This setting allows the guess to be overridden.
#! Optional.
discovery_url: #! e.g., https://example.com
#@schema/title "API serving certificate duration seconds"
#@ api_serving_certificate_duration_seconds_desc = "Specify the duration for the API serving certificate. \
#@ The default is set to expire the cert about every 30 days. \
#@ Specify this as an integer or as a string which contains an integer value."
#@schema/desc api_serving_certificate_duration_seconds_desc
#@schema/type any=True
#@schema/validation ("an int or string which contains an integer value", lambda v: type(v) in ["int", "string"])
#! Specify the duration and renewal interval for the API serving certificate.
#! The defaults are set to expire the cert about every 30 days, and to rotate it
#! about every 25 days.
api_serving_certificate_duration_seconds: 2592000
#@schema/title "API serving certificate renew before seconds"
#@ api_serving_certificate_renew_before_seconds_desc = "Specify the renewal interval for the API serving certificate. \
#@ The default is set to rotate it about every 25 days. \
#@ Specify this as an integer or as a string which contains an integer value."
#@schema/desc api_serving_certificate_renew_before_seconds_desc
#@schema/type any=True
#@schema/validation ("an int or string which contains an integer value", lambda v: type(v) in ["int", "string"])
api_serving_certificate_renew_before_seconds: 2160000
#@schema/title "Log level"
#@ log_level_desc = "Specify the verbosity of logging: info (\"nice to know\" information), debug (developer information), trace (timing information), \
#@ or all (kitchen sink). Do not use trace or all on production systems, as credentials may get logged. \
#@ When this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs."
#@schema/desc log_level_desc
#@schema/examples ("Developer logging information","debug")
#@schema/nullable
#@schema/validation one_of=["info", "debug", "trace", "all"]
log_level: ""
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer
#! information), trace (timing information), all (kitchen sink).
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
#@schema/title "Run as user"
#@schema/desc "The user ID that will own the process."
#! See the Dockerfile for the reasoning behind this default value.
run_as_user: 65532
run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice
run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice
#@schema/title "Run as group"
#@schema/desc "The group ID that will own the process."
#! See the Dockerfile for the reasoning behind this default value.
run_as_group: 65532
#@schema/title "API group suffix"
#@ api_group_suffix_desc = "Specify the API group suffix for all Pinniped API groups. By default, this is set to \
#@ pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev, \
#@ authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then \
#@ Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc."
#@schema/desc api_group_suffix_desc
#@schema/validation min_len=1
#! Specify the API group suffix for all Pinniped API groups. By default, this is set to
#! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,
#! authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then
#! Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc.
api_group_suffix: pinniped.dev
#@schema/title "Impersonation proxy spec"
#@schema/desc "Customize CredentialIssuer.spec.impersonationProxy to change how the concierge handles impersonation."
#! Customize CredentialIssuer.spec.impersonationProxy to change how the concierge
#! handles impersonation.
impersonation_proxy_spec:
#@schema/title "Mode"
#@ impersonation_mode_desc = "Enables or disables the impersonation proxy. Options are 'auto', 'disabled' or 'enabled'. \
#@ If auto, the impersonation proxy will run only if the cluster signing key is \
#@ not available and the other strategy does not work. \
#@ If enabled, the impersonation proxy will always run regardless of other strategies available. \
#@ If disabled, the impersonation proxy will never run, which could mean \
#@ that the concierge doesn't work at all."
#@schema/desc impersonation_mode_desc
#@schema/validation one_of=["auto", "disabled", "enabled"]
#! options are "auto", "disabled" or "enabled".
#! If auto, the impersonation proxy will run only if the cluster signing key is not available
#! and the other strategy does not work.
#! If disabled, the impersonation proxy will never run, which could mean that the concierge
#! doesn't work at all.
#! If enabled, the impersonation proxy will always run regardless of other strategies available.
mode: auto
#@schema/title "External endpoint"
#@ external_endpoint_desc = "The endpoint which the client should use to connect to the impersonation proxy. \
#@ If left unset, the client will default to connecting based on the ClusterIP or LoadBalancer endpoint."
#@schema/desc external_endpoint_desc
#@schema/examples ("Specified impersonation proxy endpoint", "https://1.2.3.4:5678")
#@schema/nullable
#@schema/validation min_len=1
external_endpoint: ""
#@schema/title "Service"
#@schema/desc "The impersonation proxy service configuration"
#! The endpoint which the client should use to connect to the impersonation proxy.
#! If left unset, the client will default to connecting based on the ClusterIP or LoadBalancer
#! endpoint.
external_endpoint:
service:
#@schema/title "Type"
#@ impersonation_service_type_desc = "Service backing the impersonation proxy. Options are 'LoadBalancer', 'ClusterIP' \
#@ and 'None'. LoadBalancer automatically provisions a Service of type LoadBalancer pointing at the impersonation \
#@ proxy. Some cloud providers will allocate a public IP address by default even on private clusters. ClusterIP \
#@ automatically provisions a Service of type ClusterIP pointing at the impersonation proxy. None does not provision \
#@ either and assumes that you have set the external_endpoint and set up your own ingress to connect to the impersonation proxy."
#@schema/desc impersonation_service_type_desc
#@schema/validation one_of=["LoadBalancer", "ClusterIP", "None"]
#! Options are "LoadBalancer", "ClusterIP" and "None".
#! LoadBalancer automatically provisions a Service of type LoadBalancer pointing at
#! the impersonation proxy. Some cloud providers will allocate
#! a public IP address by default even on private clusters.
#! ClusterIP automatically provisions a Service of type ClusterIP pointing at the
#! impersonation proxy.
#! None does not provision either and assumes that you have set the external_endpoint
#! and set up your own ingress to connect to the impersonation proxy.
type: LoadBalancer
#@schema/title "Annotations"
#@ annotations_desc = "The annotations that should be set on the ClusterIP or LoadBalancer Service. The default includes \
#@ a value for the AWS-specific service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout annotation, which will \
#@ be ignored except when using AWS to provide load balancer Services."
#@schema/desc annotations_desc
#@schema/nullable
#@schema/type any=True
#@schema/validation ("a map of string keys and string values", validate_strings_map)
#! The annotations that should be set on the ClusterIP or LoadBalancer Service.
annotations:
{service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "4000"}
#! When mode LoadBalancer is set, this will set the LoadBalancer Service's Spec.LoadBalancerIP.
load_balancer_ip:
#@schema/title "Load balancer IP"
#@schema/desc "When mode LoadBalancer is set, this will set the LoadBalancer Service's spec.loadBalancerIP."
#@schema/examples ("Specifying an IP", "1.2.3.4")
#@schema/nullable
#@schema/validation min_len=1
load_balancer_ip: ""
#@schema/title "HTTPS proxy"
#@ https_proxy_desc = "Set the standard golang HTTPS_PROXY and NO_PROXY environment variables on the Concierge containers. \
#@ These will be used when the Concierge makes backend-to-backend calls to authenticators using HTTPS, \
#@ e.g. when the Concierge fetches discovery documents and JWKS keys for JWTAuthenticators and POSTs to webhooks for WebhookAuthenticators. \
#@ The Concierge never makes insecure HTTP calls, so there is no reason to set HTTP_PROXY."
#@schema/desc https_proxy_desc
#@schema/examples ("Providing a proxy endpoint","http://proxy.example.com")
#@schema/nullable
#@schema/validation min_len=1
https_proxy: ""
#@schema/title "No proxy"
#@ no_proxy_desc = "Endpoints that should not be proxied. Defaults to not proxying internal Kubernetes endpoints, \
#@ localhost endpoints, and the known instance metadata IP address for public cloud providers."
#@schema/desc no_proxy_desc
no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.cluster.local"
#@schema/title "Allowed Ciphers for TLS 1.2"
#@ allowed_ciphers_for_tls_onedottwo_desc = "When specified, only the ciphers listed will be used for TLS 1.2. \
#@ This includes both server-side and client-side TLS connections. \
#@ This list must only include cipher suites that Pinniped is configured to accept \
#@ (see internal/crypto/ptls/profiles.go and internal/crypto/ptls/profiles_fips_strict.go). \
#@ Allowing too few ciphers may cause critical parts of Pinniped to be unable to function. For example, \
#@ Kubernetes pod readiness checks, Pinniped pods acting as a client to the Kubernetes API server, \
#@ Pinniped pods acting as a client to external identity providers, or Pinniped pods acting as an APIService server \
#@ all need to be able to function with the allowed TLS cipher suites. \
#@ An empty array means accept Pinniped's defaults."
#@schema/desc allowed_ciphers_for_tls_onedottwo_desc
#@schema/examples ("Example with a few secure ciphers", ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"])
#! No type, default, or validation is required here.
#! An empty array is perfectly valid, as is any array of strings.
allowed_ciphers_for_tls_onedottwo:
- ""
#@schema/title "Audit logging configuration"
#@schema/desc "Customize the content of audit log events."
audit:
#@schema/title "Log usernames and groups"
#@ log_usernames_and_groups_desc = "Enables or disables printing usernames and group names in audit logs. Options are 'enabled' or 'disabled'. \
#@ If enabled, usernames are group names may be printed in audit log events. \
#@ If disabled, usernames and group names will be redacted from audit logs because they might contain personally identifiable information."
#@schema/desc log_usernames_and_groups_desc
#@schema/validation one_of=["enabled", "disabled"]
log_usernames_and_groups: disabled
#! Set the standard golang HTTPS_PROXY and NO_PROXY environment variables on the Concierge containers.
#! These will be used when the Concierge makes backend-to-backend calls to authenticators using HTTPS,
#! e.g. when the Concierge fetches discovery documents, JWKS keys, and POSTs to token webhooks.
#! The Concierge never makes insecure HTTP calls, so there is no reason to set HTTP_PROXY.
#! Optional.
https_proxy: #! e.g. http://proxy.example.com
no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.cluster.local" #! do not proxy Kubernetes endpoints

View File

@@ -1,4 +1,4 @@
#! Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -65,26 +65,6 @@ spec:
imagePullPolicy: IfNotPresent
command:
- local-user-authenticator
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: [ "ALL" ]
#! seccompProfile was introduced in Kube v1.19. Using it on an older Kube version will result in a
#! kubectl validation error when installing via `kubectl apply`, which can be ignored using kubectl's
#! `--validate=false` flag. Note that installing via `kapp` does not complain about this validation error.
seccompProfile:
type: "RuntimeDefault"
tolerations:
- key: kubernetes.io/arch
effect: NoSchedule
operator: Equal
value: amd64 #! Allow running on amd64 nodes.
- key: kubernetes.io/arch
effect: NoSchedule
operator: Equal
value: arm64 #! Also allow running on arm64 nodes.
---
apiVersion: v1
kind: Service

View File

@@ -1,44 +1,19 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@data/values-schema
#@data/values
---
#@schema/title "Image repo"
#@schema/desc "The repository for the local-user-authenticator container image."
#@schema/validation min_len=1
image_repo: ghcr.io/vmware-tanzu/pinniped/pinniped-server
#@schema/title "Image digest"
#@schema/desc "The image digest for the local-user-authenticator container image. If both image_digest or an image_tag are given, only image_digest will be used."
#@schema/examples ("Providing a digest", "sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8")
#@schema/nullable
#@schema/validation min_len=1, when=lambda _, ctx: ctx.parent["image_tag"] == None
image_digest: ""
#@schema/title "Image tag"
#@schema/desc "The image tag for the local-user-authenticator container image. If both image_digest or an image_tag are given, only image_digest will be used."
#@schema/examples ("Providing a tag", "v0.25.0")
#@schema/validation min_len=1, when=lambda _, ctx: ctx.parent["image_digest"] == None
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
image_repo: projects.registry.vmware.com/pinniped/pinniped-server
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
image_tag: latest
#@schema/title "Image pull dockerconfigjson"
#@ image_pull_dockerconfigjson_desc = "A base64 encoded secret to be used when pulling the `image_repo` container image. \
#@ Can be used when the image_repo is a private registry. Typically, the value would be the output of: \
#@ kubectl create secret docker-registry x --docker-server=https://example.io --docker-username='USERNAME' --docker-password='PASSWORD' --dry-run=client -o json | jq -r '.data[\".dockerconfigjson\"]'"
#@schema/desc image_pull_dockerconfigjson_desc
#@ example_desc = 'base64 encoding of: {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}'
#@ example_value = "eyJhdXRocyI6eyJodHRwczovL2V4YW1wbGUuaW8iOnsidXNlcm5hbWUiOiJVU0VSTkFNRSIsInBhc3N3b3JkIjoiUEFTU1dPUkQiLCJhdXRoIjoiVlZORlVrNUJUVVU2VUVGVFUxZFBVa1E9In19fQ=="
#@schema/examples (example_desc, example_value)
#@schema/nullable
#@schema/validation min_len=1
image_pull_dockerconfigjson: ""
#! Specifies a secret to be used when pulling the above `image_repo` container image.
#! Can be used when the above image_repo is a private registry.
#! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]'
#! Optional.
image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}
#@schema/title "Run as user"
#@schema/desc "The user ID that will own the process."
#! See the Dockerfile for the reasoning behind this default value.
run_as_user: 65532
#@schema/title "Run as group"
#@schema/desc "The group ID that will own the process."
#! See the Dockerfile for the reasoning behind this default value.
run_as_group: 65532
run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice
run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice

View File

@@ -1,9 +1,11 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: federationdomains.config.supervisor.pinniped.dev
spec:
group: config.supervisor.pinniped.dev
@@ -20,7 +22,7 @@ spec:
- jsonPath: .spec.issuer
name: Issuer
type: string
- jsonPath: .status.phase
- jsonPath: .status.status
name: Status
type: string
- jsonPath: .metadata.creationTimestamp
@@ -32,286 +34,55 @@ spec:
description: FederationDomain describes the configuration of an OIDC provider.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: Spec of the OIDC provider.
properties:
identityProviders:
description: |-
IdentityProviders is the list of identity providers available for use by this FederationDomain.
An identity provider CR (e.g. OIDCIdentityProvider or LDAPIdentityProvider) describes how to connect to a server,
how to talk in a specific protocol for authentication, and how to use the schema of that server/protocol to
extract a normalized user identity. Normalized user identities include a username and a list of group names.
In contrast, IdentityProviders describes how to use that normalized identity in those Kubernetes clusters which
belong to this FederationDomain. Each entry in IdentityProviders can be configured with arbitrary transformations
on that normalized identity. For example, a transformation can add a prefix to all usernames to help avoid
accidental conflicts when multiple identity providers have different users with the same username (e.g.
"idp1:ryan" versus "idp2:ryan"). Each entry in IdentityProviders can also implement arbitrary authentication
rejection policies. Even though a user was able to authenticate with the identity provider, a policy can disallow
the authentication to the Kubernetes clusters that belong to this FederationDomain. For example, a policy could
disallow the authentication unless the user belongs to a specific group in the identity provider.
For backwards compatibility with versions of Pinniped which predate support for multiple identity providers,
an empty IdentityProviders list will cause the FederationDomain to use all available identity providers which
exist in the same namespace, but also to reject all authentication requests when there is more than one identity
provider currently defined. In this backwards compatibility mode, the name of the identity provider resource
(e.g. the Name of an OIDCIdentityProvider resource) will be used as the name of the identity provider in this
FederationDomain. This mode is provided to make upgrading from older versions easier. However, instead of
relying on this backwards compatibility mode, please consider this mode to be deprecated and please instead
explicitly list the identity provider using this IdentityProviders field.
items:
description: FederationDomainIdentityProvider describes how an identity
provider is made available in this FederationDomain.
properties:
displayName:
description: |-
DisplayName is the name of this identity provider as it will appear to clients. This name ends up in the
kubeconfig of end users, so changing the name of an identity provider that is in use by end users will be a
disruptive change for those users.
minLength: 1
type: string
objectRef:
description: |-
ObjectRef is a reference to a Pinniped identity provider resource. A valid reference is required.
If the reference cannot be resolved then the identity provider will not be made available.
Must refer to a resource of one of the Pinniped identity provider types, e.g. OIDCIdentityProvider,
LDAPIdentityProvider, ActiveDirectoryIdentityProvider.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
transforms:
description: |-
Transforms is an optional way to specify transformations to be applied during user authentication and
session refresh.
properties:
constants:
description: Constants defines constant variables and their
values which will be made available to the transform expressions.
items:
description: |-
FederationDomainTransformsConstant defines a constant variable and its value which will be made available to
the transform expressions. This is a union type, and Type is the discriminator field.
properties:
name:
description: Name determines the name of the constant.
It must be a valid identifier name.
maxLength: 64
minLength: 1
pattern: ^[a-zA-Z][_a-zA-Z0-9]*$
type: string
stringListValue:
description: StringListValue should hold the value
when Type is "stringList", and is otherwise ignored.
items:
type: string
type: array
stringValue:
description: StringValue should hold the value when
Type is "string", and is otherwise ignored.
type: string
type:
description: |-
Type determines the type of the constant, and indicates which other field should be non-empty.
Allowed values are "string" or "stringList".
enum:
- string
- stringList
type: string
required:
- name
- type
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
examples:
description: |-
Examples can optionally be used to ensure that the sequence of transformation expressions are working as
expected. Examples define sample input identities which are then run through the expression list, and the
results are compared to the expected results. If any example in this list fails, then this
identity provider will not be available for use within this FederationDomain, and the error(s) will be
added to the FederationDomain status. This can be used to help guard against programming mistakes in the
expressions, and also act as living documentation for other administrators to better understand the expressions.
items:
description: FederationDomainTransformsExample defines
a transform example.
properties:
expects:
description: |-
Expects is the expected output of the entire sequence of transforms when they are run against the
input Username and Groups.
properties:
groups:
description: Groups is the expected list of group
names after the transformations have been applied.
items:
type: string
type: array
message:
description: |-
Message is the expected error message of the transforms. When Rejected is true, then Message is the expected
message for the policy which rejected the authentication attempt. When Rejected is true and Message is blank,
then Message will be treated as the default error message for authentication attempts which are rejected by a
policy. When Rejected is false, then Message is the expected error message for some other non-policy
transformation error, such as a runtime error. When Rejected is false, there is no default expected Message.
type: string
rejected:
description: |-
Rejected is a boolean that indicates whether authentication is expected to be rejected by a policy expression
after the transformations have been applied. True means that it is expected that the authentication would be
rejected. The default value of false means that it is expected that the authentication would not be rejected
by any policy expression.
type: boolean
username:
description: Username is the expected username
after the transformations have been applied.
type: string
type: object
groups:
description: Groups is the input list of group names.
items:
type: string
type: array
username:
description: Username is the input username.
minLength: 1
type: string
required:
- expects
- username
type: object
type: array
expressions:
description: |-
Expressions are an optional list of transforms and policies to be executed in the order given during every
authentication attempt, including during every session refresh.
Each is a CEL expression. It may use the basic CEL language as defined in
https://github.com/google/cel-spec/blob/master/doc/langdef.md plus the CEL string extensions defined in
https://github.com/google/cel-go/tree/master/ext#strings.
The username and groups extracted from the identity provider, and the constants defined in this CR, are
available as variables in all expressions. The username is provided via a variable called `username` and
the list of group names is provided via a variable called `groups` (which may be an empty list).
Each user-provided constants is provided via a variable named `strConst.varName` for string constants
and `strListConst.varName` for string list constants.
The only allowed types for expressions are currently policy/v1, username/v1, and groups/v1.
Each policy/v1 must return a boolean, and when it returns false, no more expressions from the list are evaluated
and the authentication attempt is rejected.
Transformations of type policy/v1 do not return usernames or group names, and therefore cannot change the
username or group names.
Each username/v1 transform must return the new username (a string), which can be the same as the old username.
Transformations of type username/v1 do not return group names, and therefore cannot change the group names.
Each groups/v1 transform must return the new groups list (list of strings), which can be the same as the old
groups list.
Transformations of type groups/v1 do not return usernames, and therefore cannot change the usernames.
After each expression, the new (potentially changed) username or groups get passed to the following expression.
Any compilation or static type-checking failure of any expression will cause an error status on the FederationDomain.
During an authentication attempt, any unexpected runtime evaluation errors (e.g. division by zero) cause the
authentication attempt to fail. When all expressions evaluate successfully, then the (potentially changed) username
and group names have been decided for that authentication attempt.
items:
description: FederationDomainTransformsExpression defines
a transform expression.
properties:
expression:
description: Expression is a CEL expression that will
be evaluated based on the Type during an authentication.
minLength: 1
type: string
message:
description: |-
Message is only used when Type is policy/v1. It defines an error message to be used when the policy rejects
an authentication attempt. When empty, a default message will be used.
type: string
type:
description: |-
Type determines the type of the expression. It must be one of the supported types.
Allowed values are "policy/v1", "username/v1", or "groups/v1".
enum:
- policy/v1
- username/v1
- groups/v1
type: string
required:
- expression
- type
type: object
type: array
type: object
required:
- displayName
- objectRef
type: object
type: array
issuer:
description: |-
Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the
identifier that it will use for the iss claim in issued JWTs. This field will also be used as
the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is
https://example.com/foo, then your authorization endpoint will look like
https://example.com/foo/some/path/to/auth/endpoint).
See
https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information.
description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery
Metadata document, as well as the identifier that it will use for
the iss claim in issued JWTs. This field will also be used as the
base URL for any endpoints used by the OIDC Provider (e.g., if your
issuer is https://example.com/foo, then your authorization endpoint
will look like https://example.com/foo/some/path/to/auth/endpoint).
\n See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3
for more information."
minLength: 1
type: string
tls:
description: TLS specifies a secret which will contain Transport Layer
Security (TLS) configuration for the FederationDomain.
description: TLS configures how this FederationDomain is served over
Transport Layer Security (TLS).
properties:
secretName:
description: |-
SecretName is an optional name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains
the TLS serving certificate for the HTTPS endpoints served by this FederationDomain. When provided, the TLS Secret
named here must contain keys named `tls.crt` and `tls.key` that contain the certificate and private key to use
for TLS.
Server Name Indication (SNI) is an extension to the Transport Layer Security (TLS) supported by all major browsers.
SecretName is required if you would like to use different TLS certificates for issuers of different hostnames.
SNI requests do not include port numbers, so all issuers with the same DNS hostname must use the same
SecretName value even if they have different port numbers.
SecretName is not required when you would like to use only the HTTP endpoints (e.g. when the HTTP listener is
configured to listen on loopback interfaces or UNIX domain sockets for traffic from a service mesh sidecar).
It is also not required when you would like all requests to this OIDC Provider's HTTPS endpoints to
use the default TLS certificate, which is configured elsewhere.
When your Issuer URL's host is an IP address, then this field is ignored. SNI does not work for IP addresses.
description: "SecretName is an optional name of a Secret in the
same namespace, of type `kubernetes.io/tls`, which contains
the TLS serving certificate for the HTTPS endpoints served by
this FederationDomain. When provided, the TLS Secret named here
must contain keys named `tls.crt` and `tls.key` that contain
the certificate and private key to use for TLS. \n Server Name
Indication (SNI) is an extension to the Transport Layer Security
(TLS) supported by all major browsers. \n SecretName is required
if you would like to use different TLS certificates for issuers
of different hostnames. SNI requests do not include port numbers,
so all issuers with the same DNS hostname must use the same
SecretName value even if they have different port numbers. \n
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. \n When your Issuer URL's host
is an IP address, then this field is ignored. SNI does not work
for IP addresses."
type: string
type: object
required:
@@ -320,145 +91,69 @@ spec:
status:
description: Status of the OIDC provider.
properties:
conditions:
description: Conditions represent the observations of an FederationDomain's
current state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
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.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
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.
format: int64
minimum: 0
type: integer
reason:
description: |-
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.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
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])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the FederationDomain.
enum:
- Pending
- Ready
- Error
lastUpdateTime:
description: LastUpdateTime holds the time at which the Status was
last updated. It is a pointer to get around some undesirable behavior
with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811).
format: date-time
type: string
message:
description: Message provides human-readable details about the Status.
type: string
secrets:
description: Secrets contains information about this OIDC Provider's
secrets.
properties:
jwks:
description: |-
JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are
stored. If it is empty, then the signing/verification keys are either unknown or they don't
exist.
description: JWKS holds the name of the corev1.Secret in which
this OIDC Provider's signing/verification keys are stored. If
it is empty, then the signing/verification keys are either unknown
or they don't exist.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
type: object
x-kubernetes-map-type: atomic
stateEncryptionKey:
description: |-
StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
encrypting state parameters is stored.
description: StateSigningKey holds the name of the corev1.Secret
in which this OIDC Provider's key for encrypting state parameters
is stored.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
type: object
x-kubernetes-map-type: atomic
stateSigningKey:
description: |-
StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
signing state parameters is stored.
description: StateSigningKey holds the name of the corev1.Secret
in which this OIDC Provider's key for signing state parameters
is stored.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
type: object
x-kubernetes-map-type: atomic
tokenSigningKey:
description: |-
TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
signing tokens is stored.
description: TokenSigningKey holds the name of the corev1.Secret
in which this OIDC Provider's key for signing tokens is stored.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
type: object
x-kubernetes-map-type: atomic
type: object
status:
description: Status holds an enum that describes the state of this
OIDC Provider. Note that this Status can represent success or failure.
enum:
- Success
- Duplicate
- Invalid
- SameIssuerHostMustUseSameSecret
type: string
type: object
required:
- spec
@@ -467,3 +162,9 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,230 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
name: oidcclients.config.supervisor.pinniped.dev
spec:
group: config.supervisor.pinniped.dev
names:
categories:
- pinniped
kind: OIDCClient
listKind: OIDCClientList
plural: oidcclients
singular: oidcclient
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.allowedScopes[?(@ == "pinniped:request-audience")]
name: Privileged Scopes
type: string
- jsonPath: .status.totalClientSecrets
name: Client Secrets
type: integer
- jsonPath: .status.phase
name: Status
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: OIDCClient describes the configuration of an OIDC client.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: Spec of the OIDC client.
properties:
allowedGrantTypes:
description: |-
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.
items:
enum:
- authorization_code
- refresh_token
- urn:ietf:params:oauth:grant-type:token-exchange
type: string
minItems: 1
type: array
x-kubernetes-list-type: set
allowedRedirectURIs:
description: |-
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.
items:
pattern: ^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/
type: string
minItems: 1
type: array
x-kubernetes-list-type: set
allowedScopes:
description: |-
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.
items:
enum:
- openid
- offline_access
- username
- groups
- pinniped:request-audience
type: string
minItems: 1
type: array
x-kubernetes-list-type: set
tokenLifetimes:
description: tokenLifetimes are the optional overrides of token lifetimes
for an OIDCClient.
properties:
idTokenSeconds:
description: |-
idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of
ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID
tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used.
This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens
short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the
external identity provider to decide if it is acceptable for the end user to continue their session, and will
update the end user's group memberships from the external identity provider. Giving these tokens a long life is
will allow the end user to continue to use a token while avoiding these updates from the external identity
provider. However, some web applications may have reasons specific to the design of that application to prefer
longer lifetimes.
format: int32
maximum: 1800
minimum: 120
type: integer
type: object
required:
- allowedGrantTypes
- allowedRedirectURIs
- allowedScopes
type: object
status:
description: Status of the OIDC client.
properties:
conditions:
description: conditions represent the observations of an OIDCClient's
current state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
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.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
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.
format: int64
minimum: 0
type: integer
reason:
description: |-
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.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
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])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: phase summarizes the overall status of the OIDCClient.
enum:
- Pending
- Ready
- Error
type: string
totalClientSecrets:
description: totalClientSecrets is the current number of client secrets
that are detected for this OIDCClient.
format: int32
type: integer
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -1,4 +1,4 @@
#! Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -10,7 +10,6 @@
#@ "namespace",
#@ "defaultResourceName",
#@ "defaultResourceNameWithSuffix",
#@ "pinnipedDevAPIGroupWithPrefix",
#@ "getPinnipedConfigMapData",
#@ "hasUnixNetworkEndpoint",
#@ )
@@ -96,37 +95,12 @@ spec:
- /etc/config/pinniped.yaml
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: [ "ALL" ]
#! seccompProfile was introduced in Kube v1.19. Using it on an older Kube version will result in a
#! kubectl validation error when installing via `kubectl apply`, which can be ignored using kubectl's
#! `--validate=false` flag. Note that installing via `kapp` does not complain about this validation error.
seccompProfile:
type: "RuntimeDefault"
resources:
requests:
#! If OIDCClient CRs are being used, then the Supervisor needs enough CPU to run expensive bcrypt
#! operations inside the implementation of the token endpoint for any authcode flows performed by those
#! clients, so for that use case administrators may wish to increase the requests.cpu value to more
#! closely align with their anticipated needs. Increasing this value will cause Kubernetes to give more
#! available CPU to this process during times of high CPU contention. By default, don't ask for too much
#! because that would make it impossible to install the Pinniped Supervisor on small clusters.
#! Aside from performing bcrypts at the token endpoint for those clients, the Supervisor is not a
#! particularly CPU-intensive process.
cpu: "100m" #! by default, request one-tenth of a CPU
cpu: "100m"
memory: "128Mi"
limits:
#! By declaring a CPU limit that is not equal to the CPU request value, the Supervisor will be classified
#! by Kubernetes to have "burstable" quality of service.
#! See https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/#create-a-pod-that-gets-assigned-a-qos-class-of-burstable
#! If OIDCClient CRs are being used, and lots of simultaneous users have active sessions, then it is hard
#! pre-determine what the CPU limit should be for that use case. Guessing too low would cause the
#! pod's CPU usage to be throttled, resulting in poor performance. Guessing too high would allow clients
#! to cause the usage of lots of CPU resources. Administrators who have a good sense of anticipated usage
#! patterns may choose to set the requests.cpu and limits.cpu differently from these defaults.
cpu: "1000m" #! by default, throttle each pod's usage at 1 CPU
cpu: "100m"
memory: "128Mi"
volumeMounts:
- name: config-volume
@@ -141,6 +115,8 @@ spec:
readOnly: false #! writable to allow for socket use
#@ end
ports:
- containerPort: 8080
protocol: TCP
- containerPort: 8443
protocol: TCP
env:
@@ -190,15 +166,6 @@ spec:
- name: socket
emptyDir: {}
#@ end
tolerations:
- key: kubernetes.io/arch
effect: NoSchedule
operator: Equal
value: amd64 #! Allow running on amd64 nodes.
- key: kubernetes.io/arch
effect: NoSchedule
operator: Equal
value: arm64 #! Also allow running on arm64 nodes.
#! This will help make sure our multiple pods run on different nodes, making
#! our deployment "more" "HA".
affinity:
@@ -209,37 +176,3 @@ spec:
labelSelector:
matchLabels: #@ deploymentPodLabel()
topologyKey: kubernetes.io/hostname
---
apiVersion: v1
kind: Service
metadata:
#! If name is changed, must also change names.apiService in the ConfigMap above and spec.service.name in the APIService below.
name: #@ defaultResourceNameWithSuffix("api")
namespace: #@ namespace()
labels: #@ labels()
#! prevent kapp from altering the selector of our services to match kubectl behavior
annotations:
kapp.k14s.io/disable-default-label-scoping-rules: ""
spec:
type: ClusterIP
selector: #@ deploymentPodLabel()
ports:
- protocol: TCP
port: 443
targetPort: 10250
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: #@ pinnipedDevAPIGroupWithPrefix("v1alpha1.clientsecret.supervisor")
labels: #@ labels()
spec:
version: v1alpha1
group: #@ pinnipedDevAPIGroupWithPrefix("clientsecret.supervisor")
groupPriorityMinimum: 9900
versionPriority: 15
#! caBundle: Do not include this key here. Starts out null, will be updated/owned by the golang code.
service:
name: #@ defaultResourceNameWithSuffix("api")
namespace: #@ namespace()
port: 443

View File

@@ -1,4 +1,4 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -50,22 +50,11 @@ _: #@ template.replace(data.values.custom_labels)
#@ "apiGroupSuffix": data.values.api_group_suffix,
#@ "names": {
#@ "defaultTLSCertificateSecret": defaultResourceNameWithSuffix("default-tls-certificate"),
#@ "apiService": defaultResourceNameWithSuffix("api"),
#@ },
#@ "labels": labels(),
#@ "tls": {
#@ "onedottwo": {
#@ "allowedCiphers": data.values.allowed_ciphers_for_tls_onedottwo
#@ }
#@ },
#@ "audit": {
#@ "logUsernamesAndGroups": data.values.audit.log_usernames_and_groups,
#@ "logInternalPaths": data.values.audit.log_internal_paths
#@ }
#@ }
#@ if data.values.log_level:
#@ config["log"] = {}
#@ config["log"]["level"] = getAndValidateLogLevel()
#@ config["logLevel"] = getAndValidateLogLevel()
#@ end
#@ if data.values.endpoints:
#@ config["endpoints"] = data.values.endpoints

View File

@@ -1,9 +1,11 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev
spec:
group: idp.supervisor.pinniped.dev
@@ -35,19 +37,14 @@ spec:
an upstream Microsoft Active Directory identity provider.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@@ -55,16 +52,19 @@ spec:
description: Spec for configuring the identity provider.
properties:
bind:
description: |-
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.
description: 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.
properties:
secretName:
description: |-
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".
description: 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.
minLength: 1
type: string
@@ -76,83 +76,73 @@ spec:
for a user's group membership in ActiveDirectory.
properties:
attributes:
description: |-
Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as
the result of the group search.
description: Attributes specifies how the group's information
should be read from each ActiveDirectory entry which was found
as the result of the group search.
properties:
groupName:
description: |-
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.
description: 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.
type: string
type: object
base:
description: |-
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).
description: 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.
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.
type: string
filter:
description: |-
Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user.
The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the
value of an attribute of the user entry found as a result of the user search. Which attribute's
value is used to replace the placeholder(s) depends on the value of UserAttributeForFilter.
E.g. "member={}" or "&(objectClass=groupOfNames)(member={})".
For more information about ActiveDirectory filters, see https://ldap.com/ldap-filters.
Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used.
Optional. When not specified, the default will act as if the filter were specified as
"(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})".
This searches nested groups by default.
Note that nested group search can be slow for some Active Directory servers. To disable it,
you can set the filter to
"(&(objectClass=group)(member={})"
description: 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={})"
type: string
skipGroupRefresh:
description: |-
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.
description: "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. \n 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. \n 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.
\n 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."
type: boolean
userAttributeForFilter:
description: |-
UserAttributeForFilter specifies which attribute's value from the user entry found as a result of
the user search will be used to replace the "{}" placeholder(s) in the group search Filter.
For example, specifying "uid" as the UserAttributeForFilter while specifying
"&(objectClass=posixGroup)(memberUid={})" as the Filter would search for groups by replacing
the "{}" placeholder in the Filter with the value of the user's "uid" attribute.
Optional. When not specified, the default will act as if "dn" were specified. For example, leaving
UserAttributeForFilter unspecified while specifying "&(objectClass=groupOfNames)(member={})" as the Filter
would search for groups by replacing the "{}" placeholder(s) with the dn (distinguished name) of the user.
type: string
type: object
host:
description: 'Host is the hostname of this Active Directory identity
@@ -167,85 +157,55 @@ spec:
description: X.509 Certificate Authority (base64-encoded PEM bundle).
If omitted, a default set of system roots will be trusted.
type: string
certificateAuthorityDataSource:
description: |-
Reference to a CA bundle in a secret or a configmap.
Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
properties:
key:
description: |-
Key is the key name within the secret or configmap from which to read the CA bundle.
The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
certificate bundle.
minLength: 1
type: string
kind:
description: |-
Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
Allowed values are "Secret" or "ConfigMap".
"ConfigMap" uses a Kubernetes configmap to source CA Bundles.
"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
enum:
- Secret
- ConfigMap
type: string
name:
description: |-
Name is the resource name of the secret or configmap from which to read the CA bundle.
The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed.
minLength: 1
type: string
required:
- key
- kind
- name
type: object
type: object
userSearch:
description: UserSearch contains the configuration for searching for
a user by name in Active Directory.
properties:
attributes:
description: |-
Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as
the result of the user search.
description: Attributes specifies how the user's information should
be read from the ActiveDirectory entry which was found as the
result of the user search.
properties:
uid:
description: |-
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".
description: 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".
type: string
username:
description: |-
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".
description: 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".
type: string
type: object
base:
description: |-
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).
description: 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.
It may make sense to specify a subtree as a search base if you
wish to exclude some users or to make searches faster.
type: string
filter:
description: |-
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.
description: 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.
type: string
type: object
required:
@@ -258,35 +218,37 @@ spec:
description: Represents the observations of an identity provider's
current state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: 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.
properties:
lastTransitionTime:
description: |-
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.
description: 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.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
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.
description: 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.
format: int64
minimum: 0
type: integer
reason:
description: |-
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.
description: 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.
maxLength: 1024
minLength: 1
@@ -301,6 +263,10 @@ spec:
type: string
type:
description: 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)
maxLength: 316
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])$
type: string
@@ -331,3 +297,9 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,338 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
name: githubidentityproviders.idp.supervisor.pinniped.dev
spec:
group: idp.supervisor.pinniped.dev
names:
categories:
- pinniped
- pinniped-idp
- pinniped-idps
kind: GitHubIdentityProvider
listKind: GitHubIdentityProviderList
plural: githubidentityproviders
singular: githubidentityprovider
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.githubAPI.host
name: Host
type: string
- jsonPath: .status.phase
name: Status
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider.
This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App.
Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured
as OIDCClients.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: Spec for configuring the identity provider.
properties:
allowAuthentication:
description: AllowAuthentication allows customization of who can authenticate
using this IDP and how.
properties:
organizations:
description: Organizations allows customization of which organizations
can authenticate using this IDP.
properties:
allowed:
description: |-
Allowed, when specified, indicates that only users with membership in at least one of the listed
GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include
teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be
provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP.
The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations,
otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams
within that organization.
If no organizations are listed, you must set organizations: AllGitHubUsers.
items:
type: string
maxItems: 64
type: array
x-kubernetes-list-type: set
policy:
default: OnlyUsersFromAllowedOrganizations
description: |-
Allowed values are "OnlyUsersFromAllowedOrganizations" or "AllGitHubUsers".
Defaults to "OnlyUsersFromAllowedOrganizations".
Must be set to "AllGitHubUsers" if the allowed field is empty.
This field only exists to ensure that Pinniped administrators are aware that an empty list of
allowedOrganizations means all GitHub users are allowed to log in.
enum:
- OnlyUsersFromAllowedOrganizations
- AllGitHubUsers
type: string
type: object
x-kubernetes-validations:
- message: spec.allowAuthentication.organizations.policy must
be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed
has organizations listed
rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy
== ''AllGitHubUsers'')'
- message: spec.allowAuthentication.organizations.policy must
be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed
is empty
rule: '!((!has(self.allowed) || size(self.allowed) == 0) &&
self.policy == ''OnlyUsersFromAllowedOrganizations'')'
required:
- organizations
type: object
claims:
default: {}
description: Claims allows customization of the username and groups
claims.
properties:
groups:
default: slug
description: |-
Groups configures which property of the GitHub team record shall determine the group names in Kubernetes.
Can be either "name" or "slug". Defaults to "slug".
GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!").
GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins").
Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a
forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters
or single hyphens, so the first forward slash `/` will be the separator between the organization login name and
the team name or slug.
If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's
FederationDomain to further customize how these group names are presented to Kubernetes.
See the response schema for
[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user).
enum:
- name
- slug
type: string
username:
default: login:id
description: |-
Username configures which property of the GitHub user record shall determine the username in Kubernetes.
Can be either "id", "login", or "login:id". Defaults to "login:id".
GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens,
and may not start or end with hyphens. GitHub users are allowed to change their login name,
although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar",
then a second user might change their name from "baz" to "foo" in order to take the old
username of the first user. For this reason, it is not as safe to make authorization decisions
based only on the user's login attribute.
If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's
FederationDomain to further customize how these usernames are presented to Kubernetes.
Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and
unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value
from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable
choice to concatenate the two values.
See the response schema for
[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user).
enum:
- id
- login
- login:id
type: string
type: object
client:
description: Client identifies the secret with credentials for a GitHub
App or GitHub OAuth2 App (a GitHub client).
properties:
secretName:
description: |-
SecretName contains the name of a namespace-local Secret object that provides the clientID and
clientSecret for an GitHub App or GitHub OAuth2 client.
This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret".
minLength: 1
type: string
required:
- secretName
type: object
githubAPI:
default: {}
description: GitHubAPI allows configuration for GitHub Enterprise
Server
properties:
host:
default: github.com
description: |-
Host is required only for GitHub Enterprise Server.
Defaults to using GitHub's public API ("github.com").
For convenience, specifying "github.com" is equivalent to specifying "api.github.com".
Do not specify a protocol or scheme since "https://" will always be used.
Port is optional. Do not specify a path, query, fragment, or userinfo.
Only specify domain name or IP address, subdomains (optional), and port (optional).
IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address
in square brackets. Example: "[::1]:443".
minLength: 1
type: string
tls:
description: |-
TLS configuration for GitHub Enterprise Server.
Note that this field should not be needed when using GitHub's public API ("github.com").
However, if you choose to specify this field when using GitHub's public API, you must
specify a CA bundle that will verify connections to "api.github.com".
properties:
certificateAuthorityData:
description: X.509 Certificate Authority (base64-encoded PEM
bundle). If omitted, a default set of system roots will
be trusted.
type: string
certificateAuthorityDataSource:
description: |-
Reference to a CA bundle in a secret or a configmap.
Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
properties:
key:
description: |-
Key is the key name within the secret or configmap from which to read the CA bundle.
The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
certificate bundle.
minLength: 1
type: string
kind:
description: |-
Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
Allowed values are "Secret" or "ConfigMap".
"ConfigMap" uses a Kubernetes configmap to source CA Bundles.
"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
enum:
- Secret
- ConfigMap
type: string
name:
description: |-
Name is the resource name of the secret or configmap from which to read the CA bundle.
The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed.
minLength: 1
type: string
required:
- key
- kind
- name
type: object
type: object
type: object
required:
- allowAuthentication
- client
type: object
status:
description: Status of the identity provider.
properties:
conditions:
description: Conditions represents the observations of an identity
provider's current state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
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.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
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.
format: int64
minimum: 0
type: integer
reason:
description: |-
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.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
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])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the GitHubIdentityProvider.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -1,9 +1,11 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: ldapidentityproviders.idp.supervisor.pinniped.dev
spec:
group: idp.supervisor.pinniped.dev
@@ -31,24 +33,18 @@ spec:
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access
Protocol (LDAP) identity provider.
description: LDAPIdentityProvider describes the configuration of an upstream
Lightweight Directory Access Protocol (LDAP) identity provider.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@@ -56,17 +52,20 @@ spec:
description: Spec for configuring the identity provider.
properties:
bind:
description: |-
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.
description: 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.
properties:
secretName:
description: |-
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.
description: 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.
minLength: 1
type: string
required:
@@ -77,73 +76,65 @@ spec:
for a user's group membership in the LDAP provider.
properties:
attributes:
description: |-
Attributes specifies how the group's information should be read from each LDAP entry which was found as
the result of the group search.
description: Attributes specifies how the group's information
should be read from each LDAP entry which was found as the result
of the group search.
properties:
groupName:
description: |-
GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name
description: 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).
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).
type: string
type: object
base:
description: |-
Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g.
"ou=groups,dc=example,dc=com". When not specified, no group search will be performed and
authenticated users will not belong to any groups from the LDAP provider. Also, when not specified,
the values of Filter, UserAttributeForFilter, Attributes, and SkipGroupRefresh are ignored.
description: 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.
type: string
filter:
description: |-
Filter is the LDAP search filter which should be applied when searching for groups for a user.
The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the
value of an attribute of the user entry found as a result of the user search. Which attribute's
value is used to replace the placeholder(s) depends on the value of UserAttributeForFilter.
description: 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={}".
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={}".
type: string
skipGroupRefresh:
description: |-
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.
description: "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. \n 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. \n 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.
\n 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."
type: boolean
userAttributeForFilter:
description: |-
UserAttributeForFilter specifies which attribute's value from the user entry found as a result of
the user search will be used to replace the "{}" placeholder(s) in the group search Filter.
For example, specifying "uid" as the UserAttributeForFilter while specifying
"&(objectClass=posixGroup)(memberUid={})" as the Filter would search for groups by replacing
the "{}" placeholder in the Filter with the value of the user's "uid" attribute.
Optional. When not specified, the default will act as if "dn" were specified. For example, leaving
UserAttributeForFilter unspecified while specifying "&(objectClass=groupOfNames)(member={})" as the Filter
would search for groups by replacing the "{}" placeholder(s) with the dn (distinguished name) of the user.
type: string
type: object
host:
description: 'Host is the hostname of this LDAP identity provider,
@@ -158,85 +149,60 @@ spec:
description: X.509 Certificate Authority (base64-encoded PEM bundle).
If omitted, a default set of system roots will be trusted.
type: string
certificateAuthorityDataSource:
description: |-
Reference to a CA bundle in a secret or a configmap.
Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
properties:
key:
description: |-
Key is the key name within the secret or configmap from which to read the CA bundle.
The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
certificate bundle.
minLength: 1
type: string
kind:
description: |-
Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
Allowed values are "Secret" or "ConfigMap".
"ConfigMap" uses a Kubernetes configmap to source CA Bundles.
"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
enum:
- Secret
- ConfigMap
type: string
name:
description: |-
Name is the resource name of the secret or configmap from which to read the CA bundle.
The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed.
minLength: 1
type: string
required:
- key
- kind
- name
type: object
type: object
userSearch:
description: UserSearch contains the configuration for searching for
a user by name in the LDAP provider.
properties:
attributes:
description: |-
Attributes specifies how the user's information should be read from the LDAP entry which was found as
the result of the user search.
description: Attributes specifies how the user's information should
be read from the LDAP entry which was found as the result of
the user search.
properties:
uid:
description: |-
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".
description: 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".
minLength: 1
type: string
username:
description: |-
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.
description: 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.
minLength: 1
type: string
type: object
base:
description: |-
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".
description: 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".
minLength: 1
type: string
filter:
description: |-
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.
description: 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.
type: string
type: object
required:
@@ -249,35 +215,37 @@ spec:
description: Represents the observations of an identity provider's
current state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: 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.
properties:
lastTransitionTime:
description: |-
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.
description: 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.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
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.
description: 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.
format: int64
minimum: 0
type: integer
reason:
description: |-
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.
description: 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.
maxLength: 1024
minLength: 1
@@ -292,6 +260,10 @@ spec:
type: string
type:
description: 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)
maxLength: 316
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])$
type: string
@@ -322,3 +294,9 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,9 +1,11 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: oidcidentityproviders.idp.supervisor.pinniped.dev
spec:
group: idp.supervisor.pinniped.dev
@@ -35,19 +37,14 @@ spec:
OpenID Connect identity provider.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@@ -55,29 +52,39 @@ spec:
description: Spec for configuring the identity provider.
properties:
authorizationConfig:
description: |-
AuthorizationConfig holds information about how to form the OAuth2 authorization request
parameters to be used with this OIDC identity provider.
description: AuthorizationConfig holds information about how to form
the OAuth2 authorization request parameters to be used with this
OIDC identity provider.
properties:
additionalAuthorizeParameters:
description: |-
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.
description: 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.
items:
description: Parameter is a key/value pair which represents
a parameter in an HTTP request.
@@ -97,109 +104,121 @@ spec:
- name
x-kubernetes-list-type: map
additionalScopes:
description: |-
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).
description: '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).'
items:
type: string
type: array
allowPasswordGrant:
description: |-
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.
description: 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.
type: boolean
type: object
claims:
description: |-
Claims provides the names of token claims that will be used when inspecting an identity from
this OIDC identity provider.
description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider.
properties:
additionalClaimMappings:
additionalProperties:
type: string
description: |-
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.
type: object
groups:
description: |-
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.
description: 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.
type: string
username:
description: |-
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.
description: 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.
type: string
type: object
client:
description: |-
OIDCClient contains OIDC client information to be used used with this OIDC identity
provider.
description: OIDCClient contains OIDC client information to be used
used with this OIDC identity provider.
properties:
secretName:
description: |-
SecretName contains the name of a namespace-local Secret object that provides the clientID and
clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient
struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys
"clientID" and "clientSecret".
description: SecretName contains the name of a namespace-local
Secret object that provides the clientID and clientSecret for
an OIDC client. If only the SecretName is specified in an OIDCClient
struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client"
with keys "clientID" and "clientSecret".
type: string
required:
- secretName
type: object
issuer:
description: |-
Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
/.well-known/openid-configuration.
description: Issuer is the issuer URL of this OIDC identity provider,
i.e., where to fetch /.well-known/openid-configuration.
minLength: 1
pattern: ^https://
type: string
@@ -211,39 +230,6 @@ spec:
description: X.509 Certificate Authority (base64-encoded PEM bundle).
If omitted, a default set of system roots will be trusted.
type: string
certificateAuthorityDataSource:
description: |-
Reference to a CA bundle in a secret or a configmap.
Any changes to the CA bundle in the secret or configmap will be dynamically reloaded.
properties:
key:
description: |-
Key is the key name within the secret or configmap from which to read the CA bundle.
The value found at this key in the secret or configmap must not be empty, and must be a valid PEM-encoded
certificate bundle.
minLength: 1
type: string
kind:
description: |-
Kind configures whether the CA bundle is being sourced from a Kubernetes secret or a configmap.
Allowed values are "Secret" or "ConfigMap".
"ConfigMap" uses a Kubernetes configmap to source CA Bundles.
"Secret" uses Kubernetes secrets of type kubernetes.io/tls or Opaque to source CA Bundles.
enum:
- Secret
- ConfigMap
type: string
name:
description: |-
Name is the resource name of the secret or configmap from which to read the CA bundle.
The referenced secret or configmap must be created in the same namespace where Pinniped Supervisor is installed.
minLength: 1
type: string
required:
- key
- kind
- name
type: object
type: object
required:
- client
@@ -256,35 +242,37 @@ spec:
description: Represents the observations of an identity provider's
current state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: 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.
properties:
lastTransitionTime:
description: |-
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.
description: 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.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
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.
description: 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.
format: int64
minimum: 0
type: integer
reason:
description: |-
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.
description: 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.
maxLength: 1024
minLength: 1
@@ -299,6 +287,10 @@ spec:
type: string
type:
description: 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)
maxLength: 316
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])$
type: string
@@ -329,3 +321,9 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -1,4 +1,4 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -16,9 +16,6 @@ rules:
- apiGroups: [""]
resources: [secrets]
verbs: [create, get, list, patch, update, watch, delete]
- apiGroups: [""]
resources: [configmaps]
verbs: [get, list, watch]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
resources: [federationdomains]
@@ -27,14 +24,6 @@ rules:
- #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
resources: [federationdomains/status]
verbs: [get, patch, update]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
resources: [oidcclients]
verbs: [get, list, watch]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
resources: [oidcclients/status]
verbs: [get, patch, update]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
resources: [oidcidentityproviders]
@@ -59,14 +48,6 @@ rules:
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
resources: [activedirectoryidentityproviders/status]
verbs: [get, patch, update]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
resources: [githubidentityproviders]
verbs: [get, list, watch]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
resources: [githubidentityproviders/status]
verbs: [get, patch, update]
#! We want to be able to read pods/replicasets/deployment so we can learn who our deployment is to set
#! as an owner reference.
- apiGroups: [""]
@@ -93,71 +74,3 @@ roleRef:
kind: Role
name: #@ defaultResourceName()
apiGroup: rbac.authorization.k8s.io
#! Give permissions for a special configmap of CA bundles that is needed by aggregated api servers
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: #@ defaultResourceNameWithSuffix("extension-apiserver-authentication-reader")
namespace: kube-system
labels: #@ labels()
subjects:
- kind: ServiceAccount
name: #@ defaultResourceName()
namespace: #@ namespace()
roleRef:
kind: Role
name: extension-apiserver-authentication-reader
apiGroup: rbac.authorization.k8s.io
#! Give permissions for subjectaccessreviews, tokenreview that is needed by aggregated api servers
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: #@ defaultResourceName()
labels: #@ labels()
subjects:
- kind: ServiceAccount
name: #@ defaultResourceName()
namespace: #@ namespace()
roleRef:
kind: ClusterRole
name: system:auth-delegator
apiGroup: rbac.authorization.k8s.io
#! Give permission to various cluster-scoped objects
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
labels: #@ labels()
rules:
- apiGroups: [ "" ]
resources: [ namespaces ]
verbs: [ get, list, watch ]
- apiGroups: [ apiregistration.k8s.io ]
resources: [ apiservices ]
verbs: [ get, list, patch, update, watch ]
- apiGroups: [ admissionregistration.k8s.io ]
resources: [ validatingwebhookconfigurations, mutatingwebhookconfigurations, validatingadmissionpolicies, validatingadmissionpolicybindings ]
verbs: [ get, list, watch ]
- apiGroups: [ flowcontrol.apiserver.k8s.io ]
resources: [ flowschemas, prioritylevelconfigurations ]
verbs: [ get, list, watch ]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
labels: #@ labels()
subjects:
- kind: ServiceAccount
name: #@ defaultResourceName()
namespace: #@ namespace()
roleRef:
kind: ClusterRole
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
apiGroup: rbac.authorization.k8s.io

View File

@@ -1,10 +1,10 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
#@ load("helpers.lib.yaml", "labels", "deploymentPodLabel", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
#@ if data.values.service_https_nodeport_port:
#@ if data.values.service_http_nodeport_port or data.values.service_https_nodeport_port:
---
apiVersion: v1
kind: Service
@@ -19,6 +19,15 @@ spec:
type: NodePort
selector: #@ deploymentPodLabel()
ports:
#@ if data.values.service_http_nodeport_port:
- name: http
protocol: TCP
port: #@ data.values.service_http_nodeport_port
targetPort: 8080
#@ if data.values.service_http_nodeport_nodeport:
nodePort: #@ data.values.service_http_nodeport_nodeport
#@ end
#@ end
#@ if data.values.service_https_nodeport_port:
- name: https
protocol: TCP
@@ -30,7 +39,7 @@ spec:
#@ end
#@ end
#@ if data.values.service_https_clusterip_port:
#@ if data.values.service_http_clusterip_port or data.values.service_https_clusterip_port:
---
apiVersion: v1
kind: Service
@@ -45,6 +54,12 @@ spec:
type: ClusterIP
selector: #@ deploymentPodLabel()
ports:
#@ if data.values.service_http_clusterip_port:
- name: http
protocol: TCP
port: #@ data.values.service_http_clusterip_port
targetPort: 8080
#@ end
#@ if data.values.service_https_clusterip_port:
- name: https
protocol: TCP
@@ -53,7 +68,7 @@ spec:
#@ end
#@ end
#@ if data.values.service_https_loadbalancer_port:
#@ if data.values.service_http_loadbalancer_port or data.values.service_https_loadbalancer_port:
---
apiVersion: v1
kind: Service
@@ -71,6 +86,12 @@ spec:
loadBalancerIP: #@ data.values.service_loadbalancer_ip
#@ end
ports:
#@ if data.values.service_http_loadbalancer_port:
- name: http
protocol: TCP
port: #@ data.values.service_http_loadbalancer_port
targetPort: 8080
#@ end
#@ if data.values.service_https_loadbalancer_port:
- name: https
protocol: TCP

View File

@@ -1,242 +1,120 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ def validate_strings_map(obj):
#@ # Returns True if obj is an associative data structure string→string, and False otherwise.
#@ for key in obj:
#@ if type(key) != "string" or type(obj[key]) != "string":
#@ return False
#@ end
#@ end
#@ return True
#@ end
#@data/values-schema
#@data/values
---
#@schema/title "App name"
#@schema/desc "Used to help determine the names of various resources and labels."
#@schema/validation min_len=1
app_name: pinniped-supervisor
#@schema/title "Namespace"
#@schema/desc "Creates a new namespace statically in yaml with the given name and installs the app into that namespace."
#@schema/validation min_len=1
#! Creates a new namespace statically in yaml with the given name and installs the app into that namespace.
namespace: pinniped-supervisor
#! If specified, assumes that a namespace of the given name already exists and installs the app into that namespace.
#! If both `namespace` and `into_namespace` are specified, then only `into_namespace` is used.
into_namespace: #! e.g. my-preexisting-namespace
#@schema/title "Into namespace"
#@ into_namespace_desc = "If specified, assumes that a namespace of the given name already exists and installs the app into that namespace. \
#@ If both `namespace` and `into_namespace` are specified, then only `into_namespace` is used."
#@schema/desc into_namespace_desc
#@schema/examples ("The name of an existing namespace", "my-preexisting-namespace")
#@schema/nullable
#@schema/validation min_len=1
into_namespace: ""
#! All resources created statically by yaml at install-time and all resources created dynamically
#! by controllers at runtime will be labelled with `app: $app_name` and also with the labels
#! specified here. The value of `custom_labels` must be a map of string keys to string values.
#! The app can be uninstalled either by:
#! 1. Deleting the static install-time yaml resources including the static namespace, which will cascade and also delete
#! resources that were dynamically created by controllers at runtime
#! 2. Or, deleting all resources by label, which does not assume that there was a static install-time yaml namespace.
custom_labels: {} #! e.g. {myCustomLabelName: myCustomLabelValue, otherCustomLabelName: otherCustomLabelValue}
#@schema/title "Custom labels"
#@ custom_labels_desc = "All resources created statically by yaml at install-time and all resources created dynamically \
#@ by controllers at runtime will be labelled with `app: $app_name` and also with the labels specified here. The value of \
#@ `custom_labels` must be a map of string keys to string values. The app can be uninstalled either by: 1.) deleting the \
#@ static install-time yaml resources including the static namespace, which will cascade and also delete \
#@ resources that were dynamically created by controllers at runtime, or 2.) deleting all resources by label, which does \
#@ not assume that there was a static install-time yaml namespace."
#@schema/desc custom_labels_desc
#@schema/examples ("Example set of labels", {"myCustomLabelName": "myCustomLabelValue", "otherCustomLabelName": "otherCustomLabelValue"})
#@schema/type any=True
#@schema/validation ("a map of keys and values", validate_strings_map)
custom_labels: { }
#@schema/title "Replicas"
#@schema/desc "Specify how many replicas of the Pinniped server to run."
#! Specify how many replicas of the Pinniped server to run.
replicas: 2
#@schema/title "Image repo"
#@schema/desc "The repository for the Supervisor container image."
#@schema/validation min_len=1
image_repo: ghcr.io/vmware-tanzu/pinniped/pinniped-server
#@schema/title "Image digest"
#@schema/desc "The image digest for the Supervisor container image. If both image_digest or an image_tag are given, only image_digest will be used."
#@schema/examples ("Digest", "sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8")
#@schema/nullable
#@schema/validation min_len=1, when=lambda _, ctx: ctx.parent["image_tag"] == None
image_digest: ""
#@schema/title "Image tag"
#@schema/desc "The image tag for the Supervisor container image. If both image_digest or an image_tag are given, only image_digest will be used."
#@schema/examples ("Tag", "v0.25.0")
#@schema/validation min_len=1, when=lambda _, ctx: ctx.parent["image_digest"] == None
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
image_repo: projects.registry.vmware.com/pinniped/pinniped-server
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
image_tag: latest
#@schema/title "Image pull dockerconfigjson"
#@ image_pull_dockerconfigjson_desc = "A base64 encoded secret to be used when pulling the `image_repo` container image. \
#@ Can be used when the image_repo is a private registry. Typically, the value would be the output of: \
#@ kubectl create secret docker-registry x --docker-server=https://example.io --docker-username='USERNAME' --docker-password='PASSWORD' --dry-run=client -o json | jq -r '.data[\".dockerconfigjson\"]'"
#@schema/desc image_pull_dockerconfigjson_desc
#@ example_desc = 'base64 encoding of: {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}'
#@ example_value = "eyJhdXRocyI6eyJodHRwczovL2V4YW1wbGUuaW8iOnsidXNlcm5hbWUiOiJVU0VSTkFNRSIsInBhc3N3b3JkIjoiUEFTU1dPUkQiLCJhdXRoIjoiVlZORlVrNUJUVVU2VUVGVFUxZFBVa1E9In19fQ=="
#@schema/examples (example_desc, example_value)
#@schema/nullable
#@schema/validation min_len=1
image_pull_dockerconfigjson: ""
#! Specifies a secret to be used when pulling the above `image_repo` container image.
#! Can be used when the above image_repo is a private registry.
#! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]'
#! Optional.
image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}
#@schema/title "Service https nodeport port"
#@schema/desc "When specified, creates a NodePort Service with this `port` value, with port 8443 as its `targetPort`"
#@schema/examples ("Specify port",31243)
#@schema/nullable
service_https_nodeport_port: 0
#! Specify how to expose the Supervisor app's HTTP and/or HTTPS ports as a Service.
#! Typically you would set a value for only one of the following service types, for either HTTP or HTTPS depending on your needs.
#! An HTTP service should not be exposed outside the cluster. It would not be secure to serve OIDC endpoints to end users via HTTP.
#! Setting any of these values means that a Service of that type will be created.
#! Note that all port numbers should be numbers (not strings), i.e. use ytt's `--data-value-yaml` instead of `--data-value`.
service_http_nodeport_port: #! when specified, creates a NodePort Service with this `port` value, with port 8080 as its `targetPort`; e.g. 31234
service_http_nodeport_nodeport: #! the `nodePort` value of the NodePort Service, optional when `service_http_nodeport_port` is specified; e.g. 31234
service_http_loadbalancer_port: #! when specified, creates a LoadBalancer Service with this `port` value, with port 8080 as its `targetPort`; e.g. 8443
service_http_clusterip_port: #! when specified, creates a ClusterIP Service with this `port` value, with port 8080 as its `targetPort`; e.g. 8443
service_https_nodeport_port: #! when specified, creates a NodePort Service with this `port` value, with port 8443 as its `targetPort`; e.g. 31243
service_https_nodeport_nodeport: #! the `nodePort` value of the NodePort Service, optional when `service_http_nodeport_port` is specified; e.g. 31243
service_https_loadbalancer_port: #! when specified, creates a LoadBalancer Service with this `port` value, with port 8443 as its `targetPort`; e.g. 8443
service_https_clusterip_port: #! when specified, creates a ClusterIP Service with this `port` value, with port 8443 as its `targetPort`; e.g. 8443
#! The `loadBalancerIP` value of the LoadBalancer Service.
#! Ignored unless service_http_loadbalancer_port and/or service_https_loadbalancer_port are provided.
#! Optional.
service_loadbalancer_ip: #! e.g. 1.2.3.4
#@schema/title "Service https nodeport nodeport"
#@schema/desc "The `nodePort` value of the NodePort Service, optional when `service_https_nodeport_port` is specified"
#@schema/examples ("Specify port",31243)
#@schema/nullable
service_https_nodeport_nodeport: 0
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer
#! information), trace (timing information), all (kitchen sink).
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
#@schema/title "Service https loadbalancer port"
#@schema/desc "When specified, creates a LoadBalancer Service with this `port` value, with port 8443 as its `targetPort`"
#@schema/examples ("Specify port",8443)
#@schema/nullable
service_https_loadbalancer_port: 0
run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice
run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice
#@schema/title "Service https clusterip port"
#@schema/desc "When specified, creates a ClusterIP Service with this `port` value, with port 8443 as its `targetPort`"
#@schema/examples ("Specify port",8443)
#@schema/nullable
service_https_clusterip_port: 0
#@schema/title "Service loadbalancer ip"
#@schema/desc "The `loadBalancerIP` value of the LoadBalancer Service. Ignored unless service_https_loadbalancer_port is provided."
#@schema/examples ("Example IP address","1.2.3.4")
#@schema/nullable
service_loadbalancer_ip: ""
#@schema/title "Log level"
#@ log_level_desc = "Specify the verbosity of logging: info (\"nice to know\" information), debug (developer information), trace (timing information), \
#@ or all (kitchen sink). Do not use trace or all on production systems, as credentials may get logged. \
#@ When this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs."
#@schema/desc log_level_desc
#@schema/examples ("Developer logging information","debug")
#@schema/nullable
#@schema/validation one_of=["info", "debug", "trace", "all"]
log_level: ""
#@schema/title "Run as user"
#@schema/desc "The user ID that will own the process."
#! See the Dockerfile for the reasoning behind this default value.
run_as_user: 65532
#@schema/title "Run as group"
#@schema/desc "The group ID that will own the process."
#! See the Dockerfile for the reasoning behind this default value.
run_as_group: 65532
#@schema/title "API group suffix"
#@ api_group_suffix_desc = "Specify the API group suffix for all Pinniped API groups. By default, this is set to \
#@ pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev, \
#@ config.supervisor.pinniped.dev, etc. As an example, if this is set to tuna.io, then \
#@ Pinniped API groups will look like foo.tuna.io. config.supervisor.tuna.io, etc."
#@schema/desc api_group_suffix_desc
#@schema/validation min_len=1
#! Specify the API group suffix for all Pinniped API groups. By default, this is set to
#! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,
#! authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then
#! Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc.
api_group_suffix: pinniped.dev
#@schema/title "HTTPS proxy"
#@ https_proxy_desc = "Set the standard golang HTTPS_PROXY and NO_PROXY environment variables on the Supervisor containers. \
#@ These will be used when the Supervisor makes backend-to-backend calls to upstream identity providers using HTTPS, \
#@ e.g. when the Supervisor fetches discovery documents, JWKS keys, and tokens from an upstream OIDC Provider. \
#@ The Supervisor never makes insecure HTTP calls, so there is no reason to set HTTP_PROXY."
#@schema/desc https_proxy_desc
#@schema/examples ("Providing a proxy endpoint","http://proxy.example.com")
#@schema/nullable
#@schema/validation min_len=1
https_proxy: ""
#! Set the standard golang HTTPS_PROXY and NO_PROXY environment variables on the Supervisor containers.
#! These will be used when the Supervisor makes backend-to-backend calls to upstream identity providers using HTTPS,
#! e.g. when the Supervisor fetches discovery documents, JWKS keys, and tokens from an upstream OIDC Provider.
#! The Supervisor never makes insecure HTTP calls, so there is no reason to set HTTP_PROXY.
#! Optional.
https_proxy: #! e.g. http://proxy.example.com
no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.cluster.local" #! do not proxy Kubernetes endpoints
#@schema/title "No proxy"
#@ no_proxy_desc = "Endpoints that should not be proxied. Defaults to not proxying internal Kubernetes endpoints, \
#@ localhost endpoints, and the known instance metadata IP address for public cloud providers."
#@schema/desc no_proxy_desc
no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.cluster.local"
#@schema/title "Endpoints"
#@ endpoints_desc = "Control the HTTP and HTTPS listeners of the Supervisor. The current defaults are: \
#@ {\"https\":{\"network\":\"tcp\",\"address\":\":8443\"},\"http\":\"disabled\"}. \
#@ These defaults mean: 1.) for HTTPS listening, bind to all interfaces using TCP on port 8443 and \
#@ 2.) disable HTTP listening by default. \
#@ The schema of this config is as follows: \
#@ {\"https\":{\"network\":\"tcp | unix | disabled\",\"address\":\"host:port when network=tcp or /pinniped_socket/socketfile.sock when network=unix\"},\"http\":{\"network\":\"tcp | unix | disabled\",\"address\":\"same as https, except that when network=tcp then the address is only allowed to bind to loopback interfaces\"}} \
#@ The HTTP listener can only be bound to loopback interfaces. This allows the listener to accept \
#@ traffic from within the pod, e.g. from a service mesh sidecar. The HTTP listener should not be \
#@ used to accept traffic from outside the pod, since that would mean that the network traffic could be \
#@ transmitted unencrypted. The HTTPS listener should be used instead to accept traffic from outside the pod. \
#@ Ingresses and load balancers that terminate TLS connections should re-encrypt the data and route traffic \
#@ to the HTTPS listener. Unix domain sockets may also be used for integrations with service meshes. \
#@ Changing the HTTPS port number must be accompanied by matching changes to the service and deployment \
#@ manifests. Changes to the HTTPS listener must be coordinated with the deployment health checks."
#@schema/desc endpoints_desc
#@schema/examples ("Example matching default settings", '{"https":{"network":"tcp","address":":8443"},"http":"disabled"}')
#@schema/type any=True
#@ def validate_endpoint(endpoint):
#@ if(type(endpoint) not in ["yamlfragment", "string"]):
#@ return False
#@ end
#@ if(type(endpoint) in ["string"]):
#@ if (endpoint != "disabled"):
#@ return False
#@ end
#@ end
#@ if(type(endpoint) in ["yamlfragment"]):
#@ if (endpoint["network"] not in ["tcp", "unix", "disabled"]):
#@ return False
#@ end
#@ if (type(endpoint["address"]) not in ["string"]):
#@ return False
#@ end
#@ end
#@ return True
#@ end
#@ def validate_endpoints(endpoints):
#@ """
#@ Returns True if endpoints fulfill the expected structure
#@ """
#@ http_val = endpoints["http"]
#@ https_val = endpoints["https"]
#@ return validate_endpoint(http_val) and validate_endpoint(https_val)
#@ end
#@schema/nullable
#@schema/validation ("a map with keys 'http' and 'https', whose values are either the string 'disabled' or a map having keys 'network' and 'address', and the value of 'network' must be one of the allowed values", validate_endpoints)
endpoints: { }
#@schema/title "Allowed Ciphers for TLS 1.2"
#@ allowed_ciphers_for_tls_onedottwo_desc = "When specified, only the ciphers listed will be used for TLS 1.2. \
#@ This includes both server-side and client-side TLS connections. \
#@ This list must only include cipher suites that Pinniped is configured to accept \
#@ (see internal/crypto/ptls/profiles.go and internal/crypto/ptls/profiles_fips_strict.go). \
#@ Allowing too few ciphers may cause critical parts of Pinniped to be unable to function. For example, \
#@ Kubernetes pod readiness checks, Pinniped pods acting as a client to the Kubernetes API server, \
#@ Pinniped pods acting as a client to external identity providers, or Pinniped pods acting as an APIService server \
#@ all need to be able to function with the allowed TLS cipher suites. \
#@ An empty array means accept Pinniped's defaults."
#@schema/desc allowed_ciphers_for_tls_onedottwo_desc
#@schema/examples ("Example with a few secure ciphers", ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"])
#! No type, default, or validation is required here.
#! An empty array is perfectly valid, as is any array of strings.
allowed_ciphers_for_tls_onedottwo:
- ""
#@schema/title "Audit logging configuration"
#@schema/desc "Customize the content of audit log events."
audit:
#@schema/title "Log usernames and groups"
#@ log_usernames_and_groups_desc = "Enables or disables printing usernames and group names in audit logs. Options are 'enabled' or 'disabled'. \
#@ If enabled, usernames are group names may be printed in audit log events. \
#@ If disabled, usernames and group names will be redacted from audit logs because they might contain personally identifiable information."
#@schema/desc log_usernames_and_groups_desc
#@schema/validation one_of=["enabled", "disabled"]
log_usernames_and_groups: disabled
#@schema/title "Log HTTPS requests for internal paths"
#@ log_internal_paths = "Enables or disables request logging for internal paths in audit logs. Options are 'enabled' or 'disabled'. \
#@ If enabled, requests to certain paths that are typically only used internal to the cluster (e.g. /healthz) will be enabled, which can be very verbose. \
#@ If disabled, requests to those paths will not be audit logged."
#@schema/desc log_internal_paths
#@schema/validation one_of=["enabled", "disabled"]
log_internal_paths: disabled
#! Control the https and http listeners of the Supervisor.
#!
#! The schema of this config is as follows:
#!
#! endpoints:
#! https:
#! network: tcp | unix | disabled
#! address: interface:port when network=tcp or /pinniped_socket/socketfile.sock when network=unix
#! http:
#! network: same as above
#! address: same as above
#!
#! Setting network to disabled turns off that particular listener.
#! See https://pkg.go.dev/net#Listen and https://pkg.go.dev/net#Dial for a description of what can be
#! specified in the address parameter based on the given network parameter. To aid in the use of unix
#! domain sockets, a writable empty dir volume is mounted at /pinniped_socket when network is set to "unix."
#!
#! The current defaults are:
#!
#! endpoints:
#! https:
#! network: tcp
#! address: :8443
#! http:
#! network: tcp
#! address: :8080
#!
#! These defaults mean: bind to all interfaces using TCP. Use port 8443 for https and 8080 for http.
#! The defaults will change over time. Users should explicitly set this value if they wish to avoid
#! any changes on upgrade.
#!
#! A future version of the Supervisor app may include a breaking change to adjust the default
#! behavior of the http listener to only listen on 127.0.0.1 (or perhaps even to be disabled).
#!
#! Binding the http listener to addresses other than 127.0.0.1 or ::1 is deprecated.
#!
#! Unix domain sockets are recommended for integrations with service meshes. Ingresses that terminate
#! TLS connections at the edge should re-encrypt the data and route traffic to the https listener.
#!
#! Changing the port numbers used must be accompanied with matching changes to the service and deployment
#! manifests. Changes to the https listener must be coordinated with the deployment health checks.
#!
#! Optional.
endpoints:

Some files were not shown because too many files have changed in this diff Show More