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
2171 changed files with 30345 additions and 266337 deletions

View File

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

View File

@ -1,19 +0,0 @@
kind: pipeline
type: kubernetes
name: Container
steps:
- name: build & publish
image: spritsail/docker-build
context: .
settings:
repo: bv11-cr01.bessems.eu/library/pinniped-server
registry: bv11-cr01.bessems.eu
tags: latest
build_args:
- BUILDPLATFORM=linux/amd64
mtu: 1450
username:
from_secret: harbor_username
password:
from_secret: harbor_password

1
.gitattributes vendored
View File

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

View File

@ -8,18 +8,7 @@ updates:
schedule: schedule:
interval: "daily" interval: "daily"
- package-ecosystem: "gomod"
open-pull-requests-limit: 2
directory: "/hack/update-go-mod"
schedule:
interval: "daily"
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
- package-ecosystem: "docker"
directory: "/hack" # this should keep the FIPS dockerfile updated per https://github.com/dependabot/feedback/issues/145#issuecomment-414738498
schedule:
interval: "daily"

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" name: "CodeQL"
on: on:
push: push:
branches: [ "main", release* ] branches: [ main, release* ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ "main" ] branches: [ main, release* ]
schedule: schedule:
- cron: '24 3 * * 3' - cron: '39 13 * * 2'
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} runs-on: ubuntu-latest
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions: permissions:
actions: read actions: read
contents: read contents: read
@ -30,35 +25,33 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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. # 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. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# 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
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
# If the Autobuild fails above, remove it and uncomment the following three lines. # ✏️ 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. # and modify them (or add more) to build your code if your project
# uses a compiled language
# - run: | #- run: |
# echo "Run, Build Application using script" # make bootstrap
# ./location_of_script_within_repo/buildscript.sh # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v1
with:
category: "/language:${{matrix.language}}"

3
.gitignore vendored
View File

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

View File

@ -8,13 +8,16 @@ linters:
disable-all: true disable-all: true
enable: enable:
# default linters # default linters
- deadcode
- errcheck - errcheck
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unused - unused
- varcheck
# additional linters for this project (we should disable these if they get annoying). # additional linters for this project (we should disable these if they get annoying).
- asciicheck - asciicheck
@ -31,7 +34,7 @@ linters:
- godot - godot
- goheader - goheader
- goimports - goimports
- revive - golint
- goprintffuncname - goprintffuncname
- gosec - gosec
- misspell - misspell
@ -41,7 +44,7 @@ linters:
- nolintlint - nolintlint
- prealloc - prealloc
- rowserrcheck - rowserrcheck
- exportloopref - scopelint
- sqlclosecheck - sqlclosecheck
- unconvert - unconvert
- whitespace - whitespace

View File

@ -2,7 +2,7 @@
# On macOS, try `brew install pre-commit` and then run `pre-commit install`. # On macOS, try `brew install pre-commit` and then run `pre-commit install`.
exclude: '^(site|generated)/' exclude: '^(site|generated)/'
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: git://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0 rev: v3.2.0
hooks: hooks:
# TODO: find a version of this to validate ytt templates? # TODO: find a version of this to validate ytt templates?
@ -11,7 +11,6 @@ repos:
- id: check-json - id: check-json
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - 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-merge-conflict
- id: check-added-large-files - id: check-added-large-files
- id: check-byte-order-marker - id: check-byte-order-marker

View File

@ -1,8 +1,5 @@
# Contributing to Pinniped # 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. Contributions to Pinniped are welcome. Here are some things to help you get started.
## Code of Conduct ## 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). 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 ## Discussion
Got a question, comment, or idea? Please don't hesitate to reach out Got a question, comment, or idea? Please don't hesitate to reach out
via GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions), via GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions),
GitHub [Issues](https://github.com/vmware-tanzu/pinniped/issues), 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). or in the Kubernetes Slack Workspace within the [#pinniped channel](https://kubernetes.slack.com/archives/C01BW364RJA).
Join our [Google Group](https://go.pinniped.dev/community/group) to receive updates and meeting invitations.
## Issues ## 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 [FAQ](https://cla.vmware.com/faq) or submit a question through the GitHub issue
tracker. 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 ## Building
The [Dockerfile](Dockerfile) at the root of the repo can be used to build and 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: 1. Install dependencies:
- [`chromedriver`](https://chromedriver.chromium.org/) (and [Chrome](https://www.google.com/chrome/))
- [`docker`](https://www.docker.com/) - [`docker`](https://www.docker.com/)
- `htpasswd` (installed by default on MacOS, usually found in `apache2-utils` package for linux) - `htpasswd` (installed by default on MacOS, usually found in `apache2-utils` package for linux)
- [`kapp`](https://carvel.dev/#getting-started) - [`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/) - [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [`ytt`](https://carvel.dev/#getting-started) - [`ytt`](https://carvel.dev/#getting-started)
- [`nmap`](https://nmap.org/download.html) - [`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): On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
```bash ```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: 1. Create a kind cluster, compile, create container images, and install Pinniped and supporting test dependencies using:
@ -139,14 +139,9 @@ go build -o pinniped ./cmd/pinniped
1. Run the Pinniped integration tests: 1. Run the Pinniped integration tests:
```bash ```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 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. commands described above. If there are only test code changes, then simply run the tests again.

View File

@ -1,31 +1,22 @@
# syntax=docker/dockerfile:1 # syntax = docker/dockerfile:1.0-experimental
# Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. # Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# Prepare to cross-compile by always running the build stage in the build platform, not the target platform. FROM golang:1.17.7 as build-env
FROM --platform=linux/amd64 golang:1.21.3 as build-env
WORKDIR /work WORKDIR /work
COPY . .
ARG GOPROXY ARG GOPROXY
ARG KUBE_GIT_VERSION # Build the executable binary (CGO_ENABLED=0 means static linking)
ENV KUBE_GIT_VERSION=$KUBE_GIT_VERSION # Pass in GOCACHE (build cache) and GOMODCACHE (module cache) so they
# can be re-used between image builds.
# 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
# Build the statically linked (CGO_ENABLED=0) binary.
# Mount source, build cache, and module cache for performance reasons.
# See https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/
RUN \ RUN \
--mount=target=. \
--mount=type=cache,target=/cache/gocache \ --mount=type=cache,target=/cache/gocache \
--mount=type=cache,target=/cache/gomodcache \ --mount=type=cache,target=/cache/gomodcache \
export GOCACHE=/cache/gocache GOMODCACHE=/cache/gomodcache CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH && \ 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-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/... && \ 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-concierge && \
@ -33,16 +24,13 @@ RUN \
ln -s /usr/local/bin/pinniped-server /usr/local/bin/local-user-authenticator 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. # 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. FROM gcr.io/distroless/static:nonroot@sha256:80c956fb0836a17a565c43a4026c9c80b2013c83bea09f74fa4da195a59b7a99
# 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 gcr.io/distroless/static:nonroot@sha256:2a9e2b4fa771d31fe3346a873be845bfc2159695b9f90ca08e950497006ccc2e
# Copy the server binary from the build-env stage. # Copy the server binary from the build-env stage.
COPY --from=build-env /usr/local/bin /usr/local/bin COPY --from=build-env /usr/local/bin /usr/local/bin
# Document the default server ports for the various server apps # 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 # 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 # 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 | This is the current list of maintainers for the Pinniped project.
|-----------------|-----------------------------------------------------------|------------------------------------------|
| Ben Petersen | [benjaminapetersen](https://github.com/benjaminapetersen) | [VMware](https://www.github.com/vmware/) | | 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/) | | 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 ## Emeritus Maintainers
| Maintainer | GitHub ID | * Andrew Keesler, [ankeesler](https://github.com/ankeesler)
|-------------------|---------------------------------------------------------| * Pablo Schuhmacher, [pabloschuhmacher](https://github.com/pabloschuhmacher)
| Andrew Keesler | [ankeesler](https://github.com/ankeesler) | * Matt Moyer, [mattmoyer](https://github.com/mattmoyer)
| Anjali Telang | [anjaltelang](https://github.com/anjaltelang) |
| Margo Crawford | [margocrawf](https://github.com/margocrawf) | ## Pinniped Contributors & Stakeholders
| Matt Moyer | [mattmoyer](https://github.com/mattmoyer) |
| Mo Khan | [enj](https://github.com/enj) | | Feature Area | Lead |
| Pablo Schuhmacher | [pabloschuhmacher](https://github.com/pabloschuhmacher) | | ----------------------------- | :---------------------: |
| 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 Got a question, comment, or idea? Please don't hesitate to reach out
via GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions), via GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions),
GitHub [Issues](https://github.com/vmware-tanzu/pinniped/issues), 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). or in the Kubernetes Slack Workspace within the [#pinniped channel](https://kubernetes.slack.com/archives/C01BW364RJA).
Join our [Google Group](https://go.pinniped.dev/community/group) to receive updates and meeting invitations.
## Contributions ## 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. Want to get involved? Contributions are welcome.
Please see the [contributing guide](CONTRIBUTING.md) for more information about reporting bugs, requesting features, 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. 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 ## Adopters
Some organizations and products using Pinniped are featured in [ADOPTERS.md](ADOPTERS.md). 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). Pinniped is open source and licensed under Apache License Version 2.0. See [LICENSE](LICENSE).
Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.

View File

@ -1,41 +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.
The [Pinniped project backlog](https://github.com/orgs/vmware-tanzu/projects/43/) is prioritized based on this roadmap,
and it provides a more granular view of what the maintainers are working on a day-to-day basis.
### How to help ###
**About this document**
Discussion on the roadmap is welcomed. If you want to provide suggestions, use cases, and feedback to an item in the 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.
roadmap, please reach out to the maintainers using one of the methods described in the project's
[README.md](https://github.com/vmware-tanzu/pinniped#discussion).
[Contributions](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md) to Pinniped are also welcomed.
### How to add an item to the roadmap
One of the most important aspects in any open source community is the concept of proposals. Large changes to the ###
codebase and / or new features should be preceded by **How to help?**
a [proposal](https://github.com/vmware-tanzu/pinniped/tree/main/proposals) in our repo.
For smaller enhancements, you can open an issue to track that initiative or feature request.
We work with and rely on community feedback to focus our efforts to improve Pinniped and maintain a healthy roadmap.
### Current Roadmap 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.
The following table includes the current roadmap for Pinniped. Please take the timelines and dates as proposals and
goals. Priorities and requirements change based on community feedback, roadblocks encountered, community contributions,
etc. If you depend on a specific item, we encourage you to reach out for updated status information, or help us deliver
that feature by [contributing](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md) to Pinniped.
Last Updated: Sept 2022 ###
**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| |Theme|Description|Timeline|
|--|--|--| |--|--|--|
|Improving Usability|Dynamic Oauth Client Support for integrating with UI/Dashboards |Sept/Oct 2022| |Improving Security Posture|Support for refreshing LDAP/AD Group information |March 2022|
|Improving Usability|Support for custom claim mappings in OIDCIdentityProvider |Q4 2022| |Improving Security Posture|Support FIPS compliant Boring crypto libraries |March/April 2022|
|Improving Usability|Support for Multiple Identity Providers |Q4 2022| |Multiple IDP support|Support multiple IDPs configured on a single Supervisor|April 2022|
|Improving Security Posture|Support Audit logging of security events related to Authentication |Q4 2022| |Improving Security Posture|TLS hardening |March/April 2022|
|Improving Security Posture|Session Management |2022/2023| |Improving Security Posture|Support Audit logging of security events related to Authentication |April/May 2022|
|Improving Security Posture|Secrets Rotation and Management |2022/2023| |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

@ -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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -12,7 +12,7 @@ type JWTAuthenticatorStatus struct {
// +patchStrategy=merge // +patchStrategy=merge
// +listType=map // +listType=map
// +listMapKey=type // +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
} }
// Spec for configuring a JWT authenticator. // Spec for configuring a JWT authenticator.

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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -12,7 +12,7 @@ type WebhookAuthenticatorStatus struct {
// +patchStrategy=merge // +patchStrategy=merge
// +listType=map // +listType=map
// +listMapKey=type // +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
} }
// Spec for configuring a webhook authenticator. // Spec for configuring a webhook authenticator.

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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -80,28 +80,6 @@ const (
ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") 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. // ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy.
type ImpersonationProxySpec struct { type ImpersonationProxySpec struct {
// Mode configures whether the impersonation proxy should be started: // Mode configures whether the impersonation proxy should be started:
@ -122,13 +100,6 @@ type ImpersonationProxySpec struct {
// //
// +optional // +optional
ExternalEndpoint string `json:"externalEndpoint,omitempty"` 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. // ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy.

View File

@ -17,13 +17,11 @@ type WhoAmIRequest struct {
Status WhoAmIRequestStatus Status WhoAmIRequestStatus
} }
// Spec is always empty for a WhoAmIRequest.
type WhoAmIRequestSpec struct { type WhoAmIRequestSpec struct {
// empty for now but we may add some config here in the future // 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 // 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 { type WhoAmIRequestStatus struct {
// The current authenticated user, exactly as Kubernetes understands it. // The current authenticated user, exactly as Kubernetes understands it.
KubernetesUserInfo KubernetesUserInfo KubernetesUserInfo KubernetesUserInfo
@ -37,6 +35,6 @@ type WhoAmIRequestList struct {
metav1.TypeMeta metav1.TypeMeta
metav1.ListMeta metav1.ListMeta
// Items is a list of WhoAmIRequest. // Items is a list of WhoAmIRequest
Items []WhoAmIRequest Items []WhoAmIRequest
} }

View File

@ -20,13 +20,11 @@ type WhoAmIRequest struct {
Status WhoAmIRequestStatus `json:"status,omitempty"` Status WhoAmIRequestStatus `json:"status,omitempty"`
} }
// Spec is always empty for a WhoAmIRequest.
type WhoAmIRequestSpec struct { type WhoAmIRequestSpec struct {
// empty for now but we may add some config here in the future // 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 // 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 { type WhoAmIRequestStatus struct {
// The current authenticated user, exactly as Kubernetes understands it. // The current authenticated user, exactly as Kubernetes understands it.
KubernetesUserInfo KubernetesUserInfo `json:"kubernetesUserInfo"` KubernetesUserInfo KubernetesUserInfo `json:"kubernetesUserInfo"`
@ -40,6 +38,6 @@ type WhoAmIRequestList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"` metav1.ListMeta `json:"metadata,omitempty"`
// Items is a list of WhoAmIRequest. // Items is a list of WhoAmIRequest
Items []WhoAmIRequest `json:"items"` Items []WhoAmIRequest `json:"items"`
} }

View File

@ -5,8 +5,7 @@ package login
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// ClusterCredential is the cluster-specific credential returned on a successful credential request. It // ClusterCredential is a credential (token or certificate) which is valid on the Kubernetes cluster.
// contains either a valid bearer token or a valid TLS certificate and corresponding private key for the cluster.
type ClusterCredential struct { type ClusterCredential struct {
// ExpirationTimestamp indicates a time when the provided credentials expire. // ExpirationTimestamp indicates a time when the provided credentials expire.
ExpirationTimestamp metav1.Time ExpirationTimestamp metav1.Time

View File

@ -8,7 +8,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
// Specification of a TokenCredentialRequest, expected on requests to the Pinniped API.
type TokenCredentialRequestSpec struct { type TokenCredentialRequestSpec struct {
// Bearer token supplied with the credential request. // Bearer token supplied with the credential request.
Token string Token string
@ -17,9 +16,8 @@ type TokenCredentialRequestSpec struct {
Authenticator corev1.TypedLocalObjectReference Authenticator corev1.TypedLocalObjectReference
} }
// Status of a TokenCredentialRequest, returned on responses to the Pinniped API.
type TokenCredentialRequestStatus struct { type TokenCredentialRequestStatus struct {
// A Credential will be returned for a successful credential request. // A ClusterCredential will be returned for a successful credential request.
// +optional // +optional
Credential *ClusterCredential Credential *ClusterCredential
@ -44,6 +42,6 @@ type TokenCredentialRequestList struct {
metav1.TypeMeta metav1.TypeMeta
metav1.ListMeta metav1.ListMeta
// Items is a list of TokenCredentialRequest. // Items is a list of TokenCredentialRequest
Items []TokenCredentialRequest Items []TokenCredentialRequest
} }

View File

@ -8,7 +8,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
// Specification of a TokenCredentialRequest, expected on requests to the Pinniped API. // TokenCredentialRequestSpec is the specification of a TokenCredentialRequest, expected on requests to the Pinniped API.
type TokenCredentialRequestSpec struct { type TokenCredentialRequestSpec struct {
// Bearer token supplied with the credential request. // Bearer token supplied with the credential request.
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
@ -17,7 +17,7 @@ type TokenCredentialRequestSpec struct {
Authenticator corev1.TypedLocalObjectReference `json:"authenticator"` Authenticator corev1.TypedLocalObjectReference `json:"authenticator"`
} }
// Status of a TokenCredentialRequest, returned on responses to the Pinniped API. // TokenCredentialRequestStatus is the status of a TokenCredentialRequest, returned on responses to the Pinniped API.
type TokenCredentialRequestStatus struct { type TokenCredentialRequestStatus struct {
// A Credential will be returned for a successful credential request. // A Credential will be returned for a successful credential request.
// +optional // +optional
@ -47,6 +47,5 @@ type TokenCredentialRequestList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"` metav1.ListMeta `json:"metadata,omitempty"`
// Items is a list of TokenCredentialRequest.
Items []TokenCredentialRequest `json:"items"` 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

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

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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -8,17 +8,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
type FederationDomainPhase string // +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret
type FederationDomainStatusCondition string
const ( const (
// FederationDomainPhasePending is the default phase for newly-created FederationDomain resources. SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success")
FederationDomainPhasePending FederationDomainPhase = "Pending" DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate")
SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret")
// FederationDomainPhaseReady is the phase for an FederationDomain resource in a healthy state. InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid")
FederationDomainPhaseReady FederationDomainPhase = "Ready"
// FederationDomainPhaseError is the phase for an FederationDomain in an unhealthy state.
FederationDomainPhaseError FederationDomainPhase = "Error"
) )
// FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. // 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 // 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 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 // SecretName is not required when you would like to use only the HTTP endpoints (e.g. when terminating TLS at an
// configured to listen on loopback interfaces or UNIX domain sockets for traffic from a service mesh sidecar). // Ingress). It is also not required when you would like all requests to this OIDC Provider's HTTPS endpoints to
// 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. // 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. // When your Issuer URL's host is an IP address, then this field is ignored. SNI does not work for IP addresses.
@ -45,157 +41,6 @@ type FederationDomainTLSSpec struct {
SecretName string `json:"secretName,omitempty"` 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.
// +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.
// +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. // FederationDomainSpec is a struct that describes an OIDC Provider.
type FederationDomainSpec struct { type FederationDomainSpec struct {
// Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the
@ -209,35 +54,9 @@ type FederationDomainSpec struct {
// +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MinLength=1
Issuer string `json:"issuer"` 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 // +optional
TLS *FederationDomainTLSSpec `json:"tls,omitempty"` 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. // FederationDomainSecrets holds information about this OIDC Provider's secrets.
@ -266,17 +85,20 @@ type FederationDomainSecrets struct {
// FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. // FederationDomainStatus is a struct that describes the actual state of an OIDC Provider.
type FederationDomainStatus struct { type FederationDomainStatus struct {
// Phase summarizes the overall status of the FederationDomain. // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can
// +kubebuilder:default=Pending // represent success or failure.
// +kubebuilder:validation:Enum=Pending;Ready;Error // +optional
Phase FederationDomainPhase `json:"phase,omitempty"` Status FederationDomainStatusCondition `json:"status,omitempty"`
// Conditions represent the observations of an FederationDomain's current state. // Message provides human-readable details about the Status.
// +patchMergeKey=type // +optional
// +patchStrategy=merge Message string `json:"message,omitempty"`
// +listType=map
// +listMapKey=type // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // 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. // Secrets contains information about this OIDC Provider's secrets.
// +optional // +optional
@ -288,7 +110,7 @@ type FederationDomainStatus struct {
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:categories=pinniped // +kubebuilder:resource:categories=pinniped
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` // +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:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:subresource:status // +kubebuilder:subresource:status
type FederationDomain struct { type FederationDomain struct {

View File

@ -1,122 +0,0 @@
// Copyright 2022-2023 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"`
}
// 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,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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -32,7 +32,7 @@ type ActiveDirectoryIdentityProviderStatus struct {
// +patchStrategy=merge // +patchStrategy=merge
// +listType=map // +listType=map
// +listMapKey=type // +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
} }
type ActiveDirectoryIdentityProviderBind struct { 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. // 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 // 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 // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or
// value is used to replace the placeholder(s) depends on the value of UserAttributeForFilter. // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory filters, see
// E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". // https://ldap.com/ldap-filters.
// 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. // 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 // 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:={})". // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})".
@ -128,17 +127,6 @@ type ActiveDirectoryIdentityProviderGroupSearch struct {
// +optional // +optional
Filter string `json:"filter,omitempty"` 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 // Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as
// the result of the group search. // the result of the group search.
// +optional // +optional

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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -32,7 +32,7 @@ type LDAPIdentityProviderStatus struct {
// +patchStrategy=merge // +patchStrategy=merge
// +listType=map // +listType=map
// +listMapKey=type // +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
} }
type LDAPIdentityProviderBind struct { 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. // 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 // "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, // 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 // +optional
Base string `json:"base,omitempty"` Base string `json:"base,omitempty"`
// Filter is the LDAP search filter which should be applied when searching for groups for a user. // 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 // 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 // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or
// value is used to replace the placeholder(s) depends on the value of UserAttributeForFilter. // "&(objectClass=groupOfNames)(member={})". For more information about LDAP filters, see
// For more information about LDAP filters, see https://ldap.com/ldap-filters. // https://ldap.com/ldap-filters.
// Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // 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. When not specified, the default will act as if the Filter were specified as "member={}".
// +optional // +optional
Filter string `json:"filter,omitempty"` 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 // Attributes specifies how the group's information should be read from each LDAP entry which was found as
// the result of the group search. // the result of the group search.
// +optional // +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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -32,7 +32,7 @@ type OIDCIdentityProviderStatus struct {
// +patchStrategy=merge // +patchStrategy=merge
// +listType=map // +listType=map
// +listMapKey=type // +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 // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
@ -138,17 +138,6 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` 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 // OIDCClient contains information about an OIDC client (e.g., client ID and client

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

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 // SPDX-License-Identifier: Apache-2.0
// Package main is the combined entrypoint for the Pinniped "kube-cert-agent" component. // Package main is the combined entrypoint for the Pinniped "kube-cert-agent" component.
@ -8,31 +8,14 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil"
"log" "log"
"math" "math"
"os" "os"
"time" "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 ( var (
getenv = os.Getenv getenv = os.Getenv
fail = log.Fatalf fail = log.Fatalf
@ -49,11 +32,11 @@ func main() {
case "sleep": case "sleep":
sleep(math.MaxInt64) sleep(math.MaxInt64)
case "print": case "print":
certBytes, err := os.ReadFile(getenv("CERT_PATH")) certBytes, err := ioutil.ReadFile(getenv("CERT_PATH"))
if err != nil { if err != nil {
fail("could not read CERT_PATH: %v", err) 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 { if err != nil {
fail("could not read KEY_PATH: %v", err) fail("could not read KEY_PATH: %v", err)
} }

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 // SPDX-License-Identifier: Apache-2.0
// Package main is the combined entrypoint for all Pinniped server components. // Package main is the combined entrypoint for all Pinniped server components.
@ -8,23 +8,20 @@
package main package main
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog/v2"
concierge "go.pinniped.dev/internal/concierge/server" 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" lua "go.pinniped.dev/internal/localuserauthenticator"
"go.pinniped.dev/internal/plog"
supervisor "go.pinniped.dev/internal/supervisor/server" supervisor "go.pinniped.dev/internal/supervisor/server"
) )
//nolint:gochecknoglobals // these are swapped during unit tests. //nolint: gochecknoglobals // these are swapped during unit tests.
var ( var (
fail = plog.Fatal fail = klog.Fatalf
subcommands = map[string]func(){ subcommands = map[string]func(){
"pinniped-concierge": concierge.Main, "pinniped-concierge": concierge.Main,
"pinniped-supervisor": supervisor.Main, "pinniped-supervisor": supervisor.Main,
@ -34,11 +31,11 @@ var (
func main() { func main() {
if len(os.Args) == 0 { if len(os.Args) == 0 {
fail(fmt.Errorf("missing os.Args")) fail("missing os.Args")
} }
binary := filepath.Base(os.Args[0]) binary := filepath.Base(os.Args[0])
if subcommands[binary] == nil { 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]() subcommands[binary]()
} }

View File

@ -43,11 +43,8 @@ func TestEntrypoint(t *testing.T) {
var logBuf bytes.Buffer var logBuf bytes.Buffer
testLog := log.New(&logBuf, "", 0) testLog := log.New(&logBuf, "", 0)
exited := "exiting via fatal" exited := "exiting via fatal"
fail = func(err error, keysAndValues ...interface{}) { fail = func(format string, v ...interface{}) {
testLog.Print(err) testLog.Printf(format, v...)
if len(keysAndValues) > 0 {
testLog.Print(keysAndValues...)
}
panic(exited) 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,4 +1,4 @@
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. // Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -8,7 +8,7 @@ import (
"crypto/x509" "crypto/x509"
"flag" "flag"
"fmt" "fmt"
"os" "io/ioutil"
"strings" "strings"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -85,7 +85,7 @@ func (f *caBundleFlag) String() string {
} }
func (f *caBundleFlag) Set(path string) error { func (f *caBundleFlag) Set(path string) error {
pem, err := os.ReadFile(path) pem, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return fmt.Errorf("could not read CA bundle path: %w", err) return fmt.Errorf("could not read CA bundle path: %w", err)
} }

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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -6,7 +6,7 @@ package cmd
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os" "io/ioutil"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
@ -15,6 +15,7 @@ import (
configv1alpha1 "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/certauthority"
"go.pinniped.dev/internal/testutil"
) )
func TestConciergeModeFlag(t *testing.T) { func TestConciergeModeFlag(t *testing.T) {
@ -51,12 +52,12 @@ func TestConciergeModeFlag(t *testing.T) {
func TestCABundleFlag(t *testing.T) { func TestCABundleFlag(t *testing.T) {
testCA, err := certauthority.New("Test CA", 1*time.Hour) testCA, err := certauthority.New("Test CA", 1*time.Hour)
require.NoError(t, err) require.NoError(t, err)
tmpdir := t.TempDir() tmpdir := testutil.TempDir(t)
emptyFilePath := filepath.Join(tmpdir, "empty") 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") 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{} f := caBundleFlag{}
require.Equal(t, "path", f.Type()) 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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -14,7 +14,7 @@ import (
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
) )
//nolint:gochecknoinits //nolint: gochecknoinits
func init() { func init() {
rootCmd.AddCommand(generateMarkdownHelpCommand()) rootCmd.AddCommand(generateMarkdownHelpCommand())
} }
@ -24,7 +24,7 @@ func generateMarkdownHelpCommand() *cobra.Command {
Args: cobra.NoArgs, Args: cobra.NoArgs,
Use: "generate-markdown-help", Use: "generate-markdown-help",
Short: "Generate markdown help for the current set of non-hidden CLI commands", 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, Hidden: true,
RunE: runGenerateMarkdownHelp, 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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -7,14 +7,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
//nolint:gochecknoglobals //nolint: gochecknoglobals
var getCmd = &cobra.Command{ var getCmd = &cobra.Command{Use: "get", Short: "get"}
Use: "get",
Short: "Gets one of [kubeconfig]",
SilenceUsage: true, // Do not print usage message when commands fail.
}
//nolint:gochecknoinits //nolint: gochecknoinits
func init() { func init() {
rootCmd.AddCommand(getCmd) rootCmd.AddCommand(getCmd)
} }

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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -10,46 +10,47 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time" "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" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
_ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go _ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/utils/strings/slices"
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/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" conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
"go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/plog"
) )
type kubeconfigDeps struct { type kubeconfigDeps struct {
getPathToSelf func() (string, error) getPathToSelf func() (string, error)
getClientset getConciergeClientsetFunc getClientset getConciergeClientsetFunc
log plog.MinLogger log logr.Logger
} }
func kubeconfigRealDeps() kubeconfigDeps { func kubeconfigRealDeps() kubeconfigDeps {
return kubeconfigDeps{ return kubeconfigDeps{
getPathToSelf: os.Executable, getPathToSelf: os.Executable,
getClientset: getRealConciergeClientset, getClientset: getRealConciergeClientset,
log: plog.New(), log: stdr.New(log.New(os.Stderr, "", 0)),
} }
} }
//nolint:gochecknoinits //nolint: gochecknoinits
func init() { func init() {
getCmd.AddCommand(kubeconfigCommand(kubeconfigRealDeps())) getCmd.AddCommand(kubeconfigCommand(kubeconfigRealDeps()))
} }
@ -96,12 +97,6 @@ type getKubeconfigParams struct {
credentialCachePath string credentialCachePath string
credentialCachePathSet bool credentialCachePathSet bool
installHint string 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 { func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
@ -110,7 +105,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
Args: cobra.NoArgs, Args: cobra.NoArgs,
Use: "kubeconfig", Use: "kubeconfig",
Short: "Generate a Pinniped-based kubeconfig for a cluster", Short: "Generate a Pinniped-based kubeconfig for a cluster",
SilenceUsage: true, // do not print usage message when commands fail SilenceUsage: true,
} }
flags getKubeconfigParams flags getKubeconfigParams
namespace string // unused now namespace string // unused now
@ -133,9 +128,9 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
f.Var(&flags.concierge.mode, "concierge-mode", "Concierge mode of operation") 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.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.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.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.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") f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file")
@ -152,16 +147,14 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
f.StringVarP(&flags.outputPath, "output", "o", "", "Output file path (default: stdout)") 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.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.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.") 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-skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case.
"oidc-debug-session-cache", mustMarkHidden(cmd, "oidc-skip-listen")
"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",
)
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore") 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 != "" { if flags.outputPath != "" {
@ -178,15 +171,11 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
return cmd return cmd
} }
//nolint:funlen
func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, flags getKubeconfigParams) error { func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, flags getKubeconfigParams) error {
ctx, cancel := context.WithTimeout(ctx, flags.timeout) ctx, cancel := context.WithTimeout(ctx, flags.timeout)
defer cancel() 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. // Validate api group suffix and immediately return an error if it is invalid.
if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil { if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil {
return fmt.Errorf("invalid API group suffix: %w", err) return fmt.Errorf("invalid API group suffix: %w", err)
@ -241,9 +230,11 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f
cluster.CertificateAuthorityData = flags.concierge.caBundle cluster.CertificateAuthorityData = flags.concierge.caBundle
} }
if len(flags.oidc.issuer) > 0 { // If there is an issuer, and if any upstream IDP flags are not already set, then try to discover Supervisor upstream IDP details.
err = pinnipedSupervisorDiscovery(ctx, &flags, deps.log) // When all the upstream IDP flags are set by the user, then skip discovery and don't validate their input. Maybe they know something
if err != nil { // 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 return err
} }
} }
@ -271,12 +262,7 @@ func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdap
execConfig.InstallHint = flags.installHint execConfig.InstallHint = flags.installHint
var err error var err error
execConfig.Command, err = func() (string, error) { execConfig.Command, err = deps.getPathToSelf()
if flags.pinnipedCliPath != "" {
return flags.pinnipedCliPath, nil
}
return deps.getPathToSelf()
}()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not determine the Pinniped executable path: %w", err) return nil, fmt.Errorf("could not determine the Pinniped executable path: %w", err)
} }
@ -342,9 +328,6 @@ func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdap
execConfig.Args = append(execConfig.Args, "--debug-session-cache") execConfig.Args = append(execConfig.Args, "--debug-session-cache")
} }
if flags.oidc.requestAudience != "" { 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) execConfig.Args = append(execConfig.Args, "--request-audience="+flags.oidc.requestAudience)
} }
if flags.oidc.upstreamIDPName != "" { if flags.oidc.upstreamIDPName != "" {
@ -416,7 +399,7 @@ func waitForCredentialIssuer(ctx context.Context, clientset conciergeclientset.I
return credentialIssuer, nil return credentialIssuer, nil
} }
func discoverConciergeParams(credentialIssuer *configv1alpha1.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. // Autodiscover the --concierge-mode.
frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode) frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode)
if err != nil { if err != nil {
@ -464,7 +447,7 @@ func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer,
return nil return nil
} }
func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log plog.MinLogger) { func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log logr.Logger) {
for _, strategy := range credentialIssuer.Status.Strategies { for _, strategy := range credentialIssuer.Status.Strategies {
log.Info("found CredentialIssuer strategy", log.Info("found CredentialIssuer strategy",
"type", strategy.Type, "type", strategy.Type,
@ -475,7 +458,7 @@ func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log plog.M
} }
} }
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) { switch auth := authenticator.(type) {
case *conciergev1alpha1.WebhookAuthenticator: case *conciergev1alpha1.WebhookAuthenticator:
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set // If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
@ -574,7 +557,7 @@ func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.E
} }
} }
func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log plog.MinLogger) (*configv1alpha1.CredentialIssuer, error) { func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log logr.Logger) (*configv1alpha1.CredentialIssuer, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc() defer cancelFunc()
@ -600,7 +583,7 @@ func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string,
return result, nil 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) ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc() defer cancelFunc()
@ -661,7 +644,7 @@ func writeConfigAsYAML(out io.Writer, config clientcmdapi.Config) error {
return nil 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 { if flags.skipValidate {
return nil return nil
} }
@ -724,7 +707,7 @@ func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconf
log.Info("validated connection to the cluster", "attempts", attempts) log.Info("validated connection to the cluster", "attempts", attempts)
return nil 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())
} }
} }
} }
@ -732,7 +715,6 @@ func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconf
func countCACerts(pemData []byte) int { func countCACerts(pemData []byte) int {
pool := x509.NewCertPool() pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pemData) pool.AppendCertsFromPEM(pemData)
//nolint:staticcheck // since we're not using .Subjects() to access the system pool
return len(pool.Subjects()) return len(pool.Subjects())
} }
@ -745,75 +727,21 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool
return false return false
} }
func pinnipedSupervisorDiscovery(ctx context.Context, flags *getKubeconfigParams, log plog.MinLogger) error { func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams) error {
// Make a client suitable for calling the provider, which may or may not be a Pinniped Supervisor. httpClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle)
oidcProviderHTTPClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle)
if err != nil { if err != nil {
return err return err
} }
// Call the provider's discovery endpoint, but don't parse the results yet. pinnipedIDPsEndpoint, err := discoverIDPsDiscoveryEndpointURL(ctx, flags.oidc.issuer, httpClient)
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)
if err != nil { if err != nil {
return err return err
} }
if pinnipedIDPsEndpoint == "" { if pinnipedIDPsEndpoint == "" {
// The issuer is not advertising itself as a Pinniped Supervisor which supports upstream IDP discovery. // 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 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.Filter(nil, 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 false // Remove username and groups scopes if there were present in the flags.
}
return true // 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) discoveredUpstreamIDPs, err := discoverAllAvailableSupervisorUpstreamIDPs(ctx, pinnipedIDPsEndpoint, httpClient)
if err != nil { if err != nil {
return err return err
@ -831,7 +759,7 @@ func discoverSupervisorUpstreamIDP(ctx context.Context, pinnipedIDPsEndpoint str
return err return err
} }
selectedIDPFlow, err := selectUpstreamIDPFlow(discoveredIDPFlows, selectedIDPName, selectedIDPType, flags.oidc.upstreamIDPFlow, log) selectedIDPFlow, err := selectUpstreamIDPFlow(discoveredIDPFlows, selectedIDPName, selectedIDPType, flags.oidc.upstreamIDPFlow)
if err != nil { if err != nil {
return err return err
} }
@ -853,22 +781,19 @@ func newDiscoveryHTTPClient(caBundleFlag caBundleFlag) (*http.Client, error) {
return phttp.Default(rootCAs), nil return phttp.Default(rootCAs), nil
} }
func discoverIDPsDiscoveryEndpointURL(discoveredProvider *coreosoidc.Provider) (string, error) { func discoverIDPsDiscoveryEndpointURL(ctx context.Context, issuer string, httpClient *http.Client) (string, error) {
var body idpdiscoveryv1alpha1.OIDCDiscoveryResponse discoveredProvider, err := oidc.NewProvider(oidc.ClientContext(ctx, httpClient), issuer)
err := discoveredProvider.Claims(&body)
if err != nil { if err != nil {
return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err) 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 idpdiscoveryv1alpha1.OIDCDiscoveryResponse
var body discoveryResponseScopesSupported err = discoveredProvider.Claims(&body)
err := discoveredProvider.Claims(&body)
if err != nil { 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) { func discoverAllAvailableSupervisorUpstreamIDPs(ctx context.Context, pinnipedIDPsEndpoint string, httpClient *http.Client) ([]idpdiscoveryv1alpha1.PinnipedIDP, error) {
@ -888,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) 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 { if err != nil {
return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: could not read response body: %w", err) return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: could not read response body: %w", err)
} }
@ -974,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 { switch {
case len(discoveredIDPFlows) == 0: case len(discoveredIDPFlows) == 0:
// No flows listed by discovery means that we are talking to an old Supervisor from before this feature existed. // No flows listed by discovery means that we are talking to an old Supervisor from before this feature existed.
@ -998,9 +923,10 @@ func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, se
return discoveredIDPFlows[0], nil return discoveredIDPFlows[0], nil
default: default:
// The user did not specify a flow, and more than one was found. // The user did not specify a flow, and more than one was found.
log.Info("multiple client flows found, selecting first value as default", return "", fmt.Errorf(
"idpName", selectedIDPName, "idpType", selectedIDPType, "multiple client flows for Supervisor upstream identity provider %q of type %q were found, "+
"selectedFlow", discoveredIDPFlows[0].String(), "availableFlows", discoveredIDPFlows) "so the --upstream-identity-provider-flow flag must be specified. "+
return discoveredIDPFlows[0], nil "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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -7,30 +7,18 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/client-go/tools/auth/exec" "k8s.io/client-go/tools/auth/exec"
"go.pinniped.dev/internal/here"
) )
//nolint:gochecknoglobals //nolint: gochecknoglobals
var loginCmd = &cobra.Command{ var loginCmd = &cobra.Command{
Use: "login", Use: "login",
Short: "Authenticates with one of [oidc, static]", Short: "login",
Long: here.Doc( Long: "Login to a Pinniped server",
`Authenticates with one of [oidc, static]
Use "pinniped get kubeconfig" to generate a kubeconfig file which will include
one of these login subcommands in its configuration. The oidc and static
subcommands are not meant to be invoked directly by a user.
The oidc and static subcommands are Kubernetes client-go credential plugins
which are meant to be configured inside a kubeconfig file. (See the Kubernetes
authentication documentation for more information about client-go credential
plugins.)`,
),
SilenceUsage: true, // Do not print usage message when commands fail. 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() { func init() {
rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(loginCmd)
} }

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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -9,21 +9,22 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/klog/v2/klogr"
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" 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/execcredcache"
"go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
"go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/conciergeclient"
@ -32,16 +33,7 @@ import (
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
) )
const ( //nolint: gochecknoinits
// The user may override the flow selection made by `--upstream-identity-provider-flow` using an env var.
// This allows the user to override their default flow selected inside their Pinniped-compatible kubeconfig file.
// A user might want to use this env var, for example, to choose the "browser_authcode" flow when using a kubeconfig
// which specifies "cli_password" when using an IDE plugin where there is no interactive CLI available. This allows
// the user to use one kubeconfig file for both flows.
upstreamIdentityProviderFlowEnvVarName = "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW"
)
//nolint:gochecknoinits
func init() { func init() {
loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps())) loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps()))
} }
@ -89,29 +81,18 @@ type oidcLoginFlags struct {
func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
var ( var (
cmd = &cobra.Command{ cmd = &cobra.Command{
Args: cobra.NoArgs, Args: cobra.NoArgs,
Use: "oidc --issuer ISSUER", Use: "oidc --issuer ISSUER",
Short: "Login using an OpenID Connect provider", Short: "Login using an OpenID Connect provider",
Long: here.Doc( SilenceUsage: true,
`Login using an OpenID Connect provider
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
login command in its configuration. This login command is not meant to be
invoked directly by a user.
This login command is a Kubernetes client-go credential plugin which is meant to
be configured inside a kubeconfig file. (See the Kubernetes authentication
documentation for more information about client-go credential plugins.)`,
),
SilenceUsage: true, // do not print usage message when commands fail
} }
flags oidcLoginFlags flags oidcLoginFlags
conciergeNamespace string // unused now conciergeNamespace string // unused now
) )
cmd.Flags().StringVar(&flags.issuer, "issuer", "", "OpenID Connect issuer URL") 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().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.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().BoolVar(&flags.skipListen, "skip-listen", false, "Skip starting a localhost callback listener (manual copy/paste flow only)")
cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file") cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file")
@ -144,7 +125,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
} }
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { //nolint:funlen 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 { if err != nil {
plog.WarningErr("Received error while setting log level", err) plog.WarningErr("Received error while setting log level", err)
} }
@ -152,11 +133,11 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
// Initialize the session cache. // Initialize the session cache.
var sessionOptions []filesession.Option 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 { if flags.debugSessionCache {
logger := plog.WithName("session") logger := klogr.New().WithName("session")
sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) { sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) {
logger.Error("error during session cache operation", err) logger.Error(err, "error during session cache operation")
})) }))
} }
sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...) sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...)
@ -164,7 +145,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
// Initialize the login handler. // Initialize the login handler.
opts := []oidcclient.Option{ opts := []oidcclient.Option{
oidcclient.WithContext(cmd.Context()), oidcclient.WithContext(cmd.Context()),
oidcclient.WithLogger(plog.Logr()), //nolint:staticcheck // old code with lots of log statements oidcclient.WithLogger(klogr.New()),
oidcclient.WithScopes(flags.scopes), oidcclient.WithScopes(flags.scopes),
oidcclient.WithSessionCache(sessionCache), oidcclient.WithSessionCache(sessionCache),
} }
@ -185,7 +166,6 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
flowOpts, err := flowOptions( flowOpts, err := flowOptions(
idpdiscoveryv1alpha1.IDPType(flags.upstreamIdentityProviderType), idpdiscoveryv1alpha1.IDPType(flags.upstreamIdentityProviderType),
idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow), idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow),
deps,
) )
if err != nil { if err != nil {
return err return err
@ -271,21 +251,9 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred) return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
} }
func flowOptions( func flowOptions(requestedIDPType idpdiscoveryv1alpha1.IDPType, requestedFlow idpdiscoveryv1alpha1.IDPFlow) ([]oidcclient.Option, error) {
requestedIDPType idpdiscoveryv1alpha1.IDPType,
requestedFlow idpdiscoveryv1alpha1.IDPFlow,
deps oidcLoginCommandDeps,
) ([]oidcclient.Option, error) {
useCLIFlow := []oidcclient.Option{oidcclient.WithCLISendingCredentials()} useCLIFlow := []oidcclient.Option{oidcclient.WithCLISendingCredentials()}
// If the env var is set to override the --upstream-identity-provider-type flag, then override it.
flowOverride, hasFlowOverride := deps.lookupEnv(upstreamIdentityProviderFlowEnvVarName)
flowSource := "--upstream-identity-provider-flow"
if hasFlowOverride {
requestedFlow = idpdiscoveryv1alpha1.IDPFlow(flowOverride)
flowSource = upstreamIdentityProviderFlowEnvVarName
}
switch requestedIDPType { switch requestedIDPType {
case idpdiscoveryv1alpha1.IDPTypeOIDC: case idpdiscoveryv1alpha1.IDPTypeOIDC:
switch requestedFlow { switch requestedFlow {
@ -295,21 +263,19 @@ func flowOptions(
return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here
default: default:
return nil, fmt.Errorf( return nil, fmt.Errorf(
"%s value not recognized for identity provider type %q: %s (supported values: %s)", "--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)",
flowSource, requestedIDPType, requestedFlow, requestedIDPType, requestedFlow, strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String(), idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()}, ", "))
strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String(), idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()}, ", "))
} }
case idpdiscoveryv1alpha1.IDPTypeLDAP, idpdiscoveryv1alpha1.IDPTypeActiveDirectory: case idpdiscoveryv1alpha1.IDPTypeLDAP, idpdiscoveryv1alpha1.IDPTypeActiveDirectory:
switch requestedFlow { switch requestedFlow {
case idpdiscoveryv1alpha1.IDPFlowCLIPassword, "": case idpdiscoveryv1alpha1.IDPFlowCLIPassword, "":
return useCLIFlow, nil return useCLIFlow, nil
case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode: case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode:
return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here fallthrough // not supported for LDAP providers, so fallthrough to error case
default: default:
return nil, fmt.Errorf( return nil, fmt.Errorf(
"%s value not recognized for identity provider type %q: %s (supported values: %s)", "--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)",
flowSource, requestedIDPType, requestedFlow, requestedIDPType, requestedFlow, []string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()})
strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String(), idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String()}, ", "))
} }
default: default:
// Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236 // Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236
@ -328,7 +294,7 @@ func flowOptions(
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) { func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
pool := x509.NewCertPool() pool := x509.NewCertPool()
for _, p := range caBundlePaths { for _, p := range caBundlePaths {
pem, err := os.ReadFile(p) pem, err := ioutil.ReadFile(p)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not read --ca-bundle: %w", err) return nil, fmt.Errorf("could not read --ca-bundle: %w", err)
} }
@ -360,26 +326,22 @@ func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
return &cred return &cred
} }
func SetLogLevel(ctx context.Context, lookupEnv func(string) (string, bool)) (plog.Logger, error) { func SetLogLevel(lookupEnv func(string) (string, bool), prefix string) (plog.Logger, error) {
debug, _ := lookupEnv("PINNIPED_DEBUG") debug, _ := lookupEnv("PINNIPED_DEBUG")
if debug == "true" { if debug == "true" {
err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug, Format: plog.FormatCLI}) err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
logger := plog.New().WithName("pinniped-login") logger := plog.New(prefix)
return logger, nil return logger, nil
} }
/* // mustGetConfigDir returns a directory that follows the XDG base directory convention:
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.
$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should // [1] https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
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 { func mustGetConfigDir() string {
const xdgAppName = "pinniped" const xdgAppName = "pinniped"

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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -8,21 +8,20 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"os" "io/ioutil"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
clocktesting "k8s.io/utils/clock/testing" "k8s.io/klog/v2"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"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/conciergeclient"
"go.pinniped.dev/pkg/oidcclient" "go.pinniped.dev/pkg/oidcclient"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
@ -33,16 +32,12 @@ func TestLoginOIDCCommand(t *testing.T) {
testCA, err := certauthority.New("Test CA", 1*time.Hour) testCA, err := certauthority.New("Test CA", 1*time.Hour)
require.NoError(t, err) require.NoError(t, err)
tmpdir := t.TempDir() tmpdir := testutil.TempDir(t)
testCABundlePath := filepath.Join(tmpdir, "testca.pem") 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) time1 := time.Date(3020, 10, 12, 13, 14, 15, 16, time.UTC)
now, err := time.Parse(time.RFC3339Nano, "2028-10-11T23:37:26.953313745Z")
require.NoError(t, err)
nowStr := now.Local().Format(time.RFC1123)
tests := []struct { tests := []struct {
name string name string
args []string args []string
@ -61,14 +56,6 @@ func TestLoginOIDCCommand(t *testing.T) {
wantStdout: here.Doc(` wantStdout: here.Doc(`
Login using an OpenID Connect provider 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: Usage:
oidc --issuer ISSUER [flags] oidc --issuer ISSUER [flags]
@ -87,7 +74,7 @@ func TestLoginOIDCCommand(t *testing.T) {
--issuer string OpenID Connect issuer URL --issuer string OpenID Connect issuer URL
--listen-port uint16 TCP port for localhost listener (authorization code flow only) --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 --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") --session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml")
--skip-browser Skip opening the browser (just print the URL) --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-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')
@ -155,7 +142,7 @@ func TestLoginOIDCCommand(t *testing.T) {
`), `),
}, },
{ {
name: "invalid upstream type is an error", name: "invalid upstream type",
args: []string{ args: []string{
"--issuer", "test-issuer", "--issuer", "test-issuer",
"--upstream-identity-provider-type", "invalid", "--upstream-identity-provider-type", "invalid",
@ -165,18 +152,6 @@ func TestLoginOIDCCommand(t *testing.T) {
Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory) Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory)
`), `),
}, },
{
name: "invalid upstream type when flow override env var is used is still an error",
args: []string{
"--issuer", "test-issuer",
"--upstream-identity-provider-type", "invalid",
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
wantError: true,
wantStderr: here.Doc(`
Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory)
`),
},
{ {
name: "oidc upstream type with default flow is allowed", name: "oidc upstream type with default flow is allowed",
args: []string{ args: []string{
@ -212,32 +187,6 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 4, 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", wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
}, },
{
name: "oidc upstream type with CLI flow in flow override env var is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "oidc",
"--upstream-identity-provider-flow", "browser_authcode",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"},
wantOptionsCount: 5,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "oidc upstream type with with browser flow in flow override env var is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "oidc",
"--upstream-identity-provider-flow", "cli_password",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
wantOptionsCount: 4,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{ {
name: "oidc upstream type with unsupported flow is an error", name: "oidc upstream type with unsupported flow is an error",
args: []string{ args: []string{
@ -252,21 +201,6 @@ func TestLoginOIDCCommand(t *testing.T) {
Error: --upstream-identity-provider-flow value not recognized for identity provider type "oidc": foobar (supported values: browser_authcode, cli_password) Error: --upstream-identity-provider-flow value not recognized for identity provider type "oidc": foobar (supported values: browser_authcode, cli_password)
`), `),
}, },
{
name: "oidc upstream type with unsupported flow in flow override env var is an error",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "oidc",
"--upstream-identity-provider-flow", "browser_authcode",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"},
wantError: true,
wantStderr: here.Doc(`
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "oidc": foo (supported values: browser_authcode, cli_password)
`),
},
{ {
name: "ldap upstream type with default flow is allowed", name: "ldap upstream type with default flow is allowed",
args: []string{ args: []string{
@ -301,71 +235,18 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 5, 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", wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
}, },
{
name: "ldap upstream type with browser_authcode flow is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "ldap",
"--upstream-identity-provider-flow", "browser_authcode",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
wantOptionsCount: 4,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "ldap upstream type with CLI flow in flow override env var is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "ldap",
"--upstream-identity-provider-flow", "browser_authcode",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"},
wantOptionsCount: 5,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "ldap upstream type with browser_authcode flow in flow override env var is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "ldap",
"--upstream-identity-provider-flow", "cli_password",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
wantOptionsCount: 4,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{ {
name: "ldap upstream type with unsupported flow is an error", name: "ldap upstream type with unsupported flow is an error",
args: []string{ args: []string{
"--issuer", "test-issuer", "--issuer", "test-issuer",
"--client-id", "test-client-id", "--client-id", "test-client-id",
"--upstream-identity-provider-type", "ldap", "--upstream-identity-provider-type", "ldap",
"--upstream-identity-provider-flow", "foo", "--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 "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
}, },
wantError: true, wantError: true,
wantStderr: here.Doc(` wantStderr: here.Doc(`
Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode) Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": browser_authcode (supported values: [cli_password])
`),
},
{
name: "ldap upstream type with unsupported flow in flow override env var is an error",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "ldap",
"--upstream-identity-provider-flow", "browser_authcode",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"},
wantError: true,
wantStderr: here.Doc(`
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode)
`), `),
}, },
{ {
@ -380,71 +261,18 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 5, 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", wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
}, },
{
name: "active directory upstream type with browser_authcode is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "activedirectory",
"--upstream-identity-provider-flow", "browser_authcode",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
wantOptionsCount: 4,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "active directory upstream type with CLI flow in flow override env var is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "activedirectory",
"--upstream-identity-provider-flow", "browser_authcode",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"},
wantOptionsCount: 5,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
name: "active directory upstream type with browser_authcode in flow override env var is allowed",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "activedirectory",
"--upstream-identity-provider-flow", "cli_password",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
wantOptionsCount: 4,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{ {
name: "active directory upstream type with unsupported flow is an error", name: "active directory upstream type with unsupported flow is an error",
args: []string{ args: []string{
"--issuer", "test-issuer", "--issuer", "test-issuer",
"--client-id", "test-client-id", "--client-id", "test-client-id",
"--upstream-identity-provider-type", "activedirectory", "--upstream-identity-provider-type", "activedirectory",
"--upstream-identity-provider-flow", "foo", "--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 "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
}, },
wantError: true, wantError: true,
wantStderr: here.Doc(` wantStderr: here.Doc(`
Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode) Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": browser_authcode (supported values: [cli_password])
`),
},
{
name: "active directory upstream type with unsupported flow in flow override env var is an error",
args: []string{
"--issuer", "test-issuer",
"--client-id", "test-client-id",
"--upstream-identity-provider-type", "activedirectory",
"--upstream-identity-provider-flow", "browser_authcode",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
},
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"},
wantError: true,
wantStderr: here.Doc(`
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode)
`), `),
}, },
{ {
@ -490,8 +318,8 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 4, 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", 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{ wantLogs: []string{
nowStr + ` pinniped-login cmd/login_oidc.go:243 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, "\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"",
nowStr + ` pinniped-login cmd/login_oidc.go:263 No concierge configured, skipping token credential exchange`, "\"level\"=0 \"msg\"=\"Pinniped login: No concierge configured, skipping token credential exchange\"",
}, },
}, },
{ {
@ -512,7 +340,7 @@ func TestLoginOIDCCommand(t *testing.T) {
"--concierge-endpoint", "https://127.0.0.1:1234/", "--concierge-endpoint", "https://127.0.0.1:1234/",
"--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()), "--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()),
"--concierge-api-group-suffix", "some.suffix.com", "--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-name", "some-upstream-name",
"--upstream-identity-provider-type", "ldap", "--upstream-identity-provider-type", "ldap",
}, },
@ -520,20 +348,18 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 11, wantOptionsCount: 11,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n", wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n",
wantLogs: []string{ wantLogs: []string{
nowStr + ` pinniped-login cmd/login_oidc.go:243 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, "\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"",
nowStr + ` pinniped-login cmd/login_oidc.go:253 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`, "\"level\"=0 \"msg\"=\"Pinniped login: Exchanging token for cluster credential\" \"authenticator name\"=\"test-authenticator\" \"authenticator type\"=\"webhook\" \"endpoint\"=\"https://127.0.0.1:1234/\"",
nowStr + ` pinniped-login cmd/login_oidc.go:261 Successfully exchanged token for cluster credential.`, "\"level\"=0 \"msg\"=\"Pinniped login: Successfully exchanged token for cluster credential.\"",
nowStr + ` pinniped-login cmd/login_oidc.go:268 caching cluster credential for future use.`, "\"level\"=0 \"msg\"=\"Pinniped login: caching cluster credential for future use.\"",
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
fakeClock := clocktesting.NewFakeClock(now) klog.SetLogger(testLogger.Logger)
ctx := plog.TestZapOverrides(context.Background(), t, &buf, nil, zap.WithClock(plog.ZapClock(fakeClock)))
var ( var (
gotOptions []oidcclient.Option gotOptions []oidcclient.Option
) )
@ -578,7 +404,7 @@ func TestLoginOIDCCommand(t *testing.T) {
cmd.SetOut(&stdout) cmd.SetOut(&stdout)
cmd.SetErr(&stderr) cmd.SetErr(&stderr)
cmd.SetArgs(tt.args) cmd.SetArgs(tt.args)
err = cmd.ExecuteContext(ctx) err := cmd.Execute()
if tt.wantError { if tt.wantError {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -588,15 +414,7 @@ func TestLoginOIDCCommand(t *testing.T) {
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
require.Len(t, gotOptions, tt.wantOptionsCount) require.Len(t, gotOptions, tt.wantOptionsCount)
require.Equal(t, tt.wantLogs, logLines(buf.String())) require.Equal(t, tt.wantLogs, testLogger.Lines())
}) })
} }
} }
func logLines(logs string) []string {
if len(logs) == 0 {
return nil
}
return strings.Split(strings.TrimSpace(logs), "\n")
}

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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -7,6 +7,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -16,13 +17,12 @@ import (
"go.pinniped.dev/internal/execcredcache" "go.pinniped.dev/internal/execcredcache"
"go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
"go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/conciergeclient"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
) )
//nolint:gochecknoinits //nolint: gochecknoinits
func init() { func init() {
loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps())) loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps()))
} }
@ -56,21 +56,10 @@ type staticLoginParams struct {
func staticLoginCommand(deps staticLoginDeps) *cobra.Command { func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
var ( var (
cmd = &cobra.Command{ cmd = &cobra.Command{
Args: cobra.NoArgs, Args: cobra.NoArgs,
Use: "static [--token TOKEN] [--token-env TOKEN_NAME]", Use: "static [--token TOKEN] [--token-env TOKEN_NAME]",
Short: "Login using a static token", Short: "Login using a static token",
Long: here.Doc( SilenceUsage: true,
`Login using a static token
Use "pinniped get kubeconfig" to generate a kubeconfig file which includes this
login command in its configuration. This login command is not meant to be
invoked directly by a user.
This login command is a Kubernetes client-go credential plugin which is meant to
be configured inside a kubeconfig file. (See the Kubernetes authentication
documentation for more information about client-go credential plugins.)`,
),
SilenceUsage: true, // do not print usage message when commands fail
} }
flags staticLoginParams flags staticLoginParams
conciergeNamespace string // unused now 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.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.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") mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
mustMarkHidden(cmd, "concierge-namespace") mustMarkHidden(cmd, "concierge-namespace")
@ -94,9 +83,8 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
return cmd return cmd
} }
func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginParams) error { func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error {
out := cmd.OutOrStdout() pLogger, err := SetLogLevel(deps.lookupEnv, "Pinniped login: ")
pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv)
if err != nil { if err != nil {
plog.WarningErr("Received error while setting log level", err) plog.WarningErr("Received error while setting log level", err)
} }

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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -7,20 +7,22 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"os" "io/ioutil"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/testutil/testlogger"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
clocktesting "k8s.io/utils/clock/testing"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/conciergeclient"
) )
@ -29,13 +31,9 @@ func TestLoginStaticCommand(t *testing.T) {
testCA, err := certauthority.New("Test CA", 1*time.Hour) testCA, err := certauthority.New("Test CA", 1*time.Hour)
require.NoError(t, err) require.NoError(t, err)
tmpdir := t.TempDir() tmpdir := testutil.TempDir(t)
testCABundlePath := filepath.Join(tmpdir, "testca.pem") testCABundlePath := filepath.Join(tmpdir, "testca.pem")
require.NoError(t, os.WriteFile(testCABundlePath, testCA.Bundle(), 0600)) require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
now, err := time.Parse(time.RFC3339Nano, "2038-12-07T23:37:26.953313745Z")
require.NoError(t, err)
nowStr := now.Local().Format(time.RFC1123)
tests := []struct { tests := []struct {
name string name string
@ -55,14 +53,6 @@ func TestLoginStaticCommand(t *testing.T) {
wantStdout: here.Doc(` wantStdout: here.Doc(`
Login using a static token 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: Usage:
static [--token TOKEN] [--token-env TOKEN_NAME] [flags] static [--token TOKEN] [--token-env TOKEN_NAME] [flags]
@ -146,9 +136,7 @@ func TestLoginStaticCommand(t *testing.T) {
wantStderr: here.Doc(` wantStderr: here.Doc(`
Error: could not complete Concierge credential exchange: some concierge error Error: could not complete Concierge credential exchange: some concierge error
`), `),
wantLogs: []string{ 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/\""},
nowStr + ` pinniped-login cmd/login_static.go:159 exchanging static token for cluster credential {"endpoint": "https://127.0.0.1/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
},
}, },
{ {
name: "invalid API group suffix", name: "invalid API group suffix",
@ -177,10 +165,8 @@ func TestLoginStaticCommand(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
fakeClock := clocktesting.NewFakeClock(now) klog.SetLogger(testLogger.Logger)
ctx := plog.TestZapOverrides(context.Background(), t, &buf, nil, zap.WithClock(plog.ZapClock(fakeClock)))
cmd := staticLoginCommand(staticLoginDeps{ cmd := staticLoginCommand(staticLoginDeps{
lookupEnv: func(s string) (string, bool) { lookupEnv: func(s string) (string, bool) {
v, ok := tt.env[s] v, ok := tt.env[s]
@ -208,7 +194,7 @@ func TestLoginStaticCommand(t *testing.T) {
cmd.SetOut(&stdout) cmd.SetOut(&stdout)
cmd.SetErr(&stderr) cmd.SetErr(&stderr)
cmd.SetArgs(tt.args) cmd.SetArgs(tt.args)
err := cmd.ExecuteContext(ctx) err := cmd.Execute()
if tt.wantError { if tt.wantError {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -217,7 +203,7 @@ func TestLoginStaticCommand(t *testing.T) {
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout") require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
require.Equal(t, tt.wantLogs, logLines(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,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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
import ( import (
"context" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog"
) )
//nolint:gochecknoglobals //nolint: gochecknoglobals
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "pinniped", Use: "pinniped",
Long: here.Doc( Short: "pinniped",
`The Pinniped CLI is the client-side binary for use with Pinniped-enabled Kubernetes clusters Long: "pinniped is the client-side binary for use with Pinniped-enabled Kubernetes clusters.",
Find more information at: https://pinniped.dev`,
),
SilenceUsage: true, // do not print usage message when commands fail SilenceUsage: true, // do not print usage message when commands fail
} }
// Execute adds all child commands to the root command and sets flags appropriately. // 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. // This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() error { func Execute() {
defer plog.Setup()() if err := rootCmd.Execute(); err != nil {
// the context does not matter here because it is unused when CLI formatting is provided os.Exit(1)
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(context.Background(), plog.LogSpec{Format: plog.FormatCLI}); err != nil {
return err
} }
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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/yaml" "k8s.io/component-base/version"
"go.pinniped.dev/internal/pversion"
) )
//nolint:gochecknoinits //nolint: gochecknoinits
func init() { func init() {
rootCmd.AddCommand(newVersionCommand()) rootCmd.AddCommand(newVersionCommand())
} }
//nolint:gochecknoglobals
var (
output = new(string)
// getBuildInfo can be overwritten by tests.
getBuildInfo = pversion.Get
)
func newVersionCommand() *cobra.Command { func newVersionCommand() *cobra.Command {
c := &cobra.Command{ return &cobra.Command{
RunE: runner, 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 Args: cobra.NoArgs, // do not accept positional arguments for this command
Use: "version", Use: "version",
Short: "Print the version of this Pinniped CLI", 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-2023 the Pinniped contributors. All Rights Reserved. // Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -9,10 +9,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
apimachineryversion "k8s.io/apimachinery/pkg/version"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/pversion"
) )
var ( var (
@ -21,8 +19,8 @@ var (
version \[flags\] version \[flags\]
Flags: Flags:
-h, --help help for version -h, --help help for version
-o, --output string one of 'yaml' or 'json'
`) `)
knownGoodHelpRegexpForVersion = here.Doc(` knownGoodHelpRegexpForVersion = here.Doc(`
@ -32,55 +30,24 @@ var (
version \[flags\] version \[flags\]
Flags: Flags:
-h, --help help for version -h, --help help for version
-o, --output string one of 'yaml' or 'json'
`) `)
jsonRegexp = here.Doc(`{ emptyVersionRegexp = `version.Info{Major:"", Minor:"", GitVersion:".*", GitCommit:".*", GitTreeState:"", BuildDate:".*", GoVersion:".*", Compiler:".*", Platform:".*/.*"}`
"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: .*/.*
`)
) )
func TestNewVersionCmd(t *testing.T) { func TestNewVersionCmd(t *testing.T) {
t.Cleanup(func() {
getBuildInfo = pversion.Get
})
tests := []struct { tests := []struct {
name string name string
args []string args []string
vars string
getBuildInfo func() apimachineryversion.Info
wantError bool wantError bool
wantStdoutRegexp string wantStdoutRegexp string
wantStderrRegexp string wantStderrRegexp string
}{ }{
{ {
name: "no flags", name: "no flags",
args: []string{}, args: []string{},
getBuildInfo: func() apimachineryversion.Info { wantStdoutRegexp: emptyVersionRegexp + "\n",
return apimachineryversion.Info{GitVersion: "v55.66.44"}
},
wantStdoutRegexp: "v55.66.44\n",
}, },
{ {
name: "help flag passed", name: "help flag passed",
@ -94,44 +61,10 @@ func TestNewVersionCmd(t *testing.T) {
wantStderrRegexp: `Error: unknown command "tuna" for "version"`, wantStderrRegexp: `Error: unknown command "tuna" for "version"`,
wantStdoutRegexp: knownGoodUsageRegexpForVersion, 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 { for _, tt := range tests {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if tt.getBuildInfo != nil {
getBuildInfo = tt.getBuildInfo
}
cmd := newVersionCommand() cmd := newVersionCommand()
require.NotNil(t, cmd) require.NotNil(t, cmd)

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 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -24,7 +24,7 @@ import (
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
) )
//nolint:gochecknoinits //nolint: gochecknoinits
func init() { func init() {
rootCmd.AddCommand(newWhoamiCommand(getRealConciergeClientset)) rootCmd.AddCommand(newWhoamiCommand(getRealConciergeClientset))
} }
@ -48,7 +48,7 @@ func newWhoamiCommand(getClientset getConciergeClientsetFunc) *cobra.Command {
Args: cobra.NoArgs, // do not accept positional arguments for this command Args: cobra.NoArgs, // do not accept positional arguments for this command
Use: "whoami", Use: "whoami",
Short: "Print information about the current user", Short: "Print information about the current user",
SilenceUsage: true, // do not print usage message when commands fail SilenceUsage: true,
} }
flags := &whoamiFlags{} flags := &whoamiFlags{}

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 // SPDX-License-Identifier: Apache-2.0
package main package main
@ -9,11 +9,9 @@ import (
"github.com/pkg/browser" "github.com/pkg/browser"
"go.pinniped.dev/cmd/pinniped/cmd" "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() { func init() {
// browsers like chrome like to write to our std out which breaks our JSON ExecCredential output // 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 // thus we redirect the browser's std out to our std err
@ -21,7 +19,5 @@ func init() {
} }
func main() { func main() {
if err := cmd.Execute(); err != nil { cmd.Execute()
os.Exit(1)
}
} }

View File

@ -1,9 +1,11 @@
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.13.0 controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: jwtauthenticators.authentication.concierge.pinniped.dev name: jwtauthenticators.authentication.concierge.pinniped.dev
spec: spec:
group: authentication.concierge.pinniped.dev group: authentication.concierge.pinniped.dev
@ -96,15 +98,9 @@ spec:
description: Represents the observations of the authenticator's current description: Represents the observations of the authenticator's current
state. state.
items: items:
description: "Condition contains details for one aspect of the current description: Condition status of a resource (mirrored from the metav1.Condition
state of this API Resource. --- This struct is intended for direct type added in Kubernetes 1.19). In a future API version we can
use as an array at the field path .status.conditions. For example, switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties: properties:
lastTransitionTime: lastTransitionTime:
description: lastTransitionTime is the last time the condition description: lastTransitionTime is the last time the condition
@ -173,3 +169,9 @@ spec:
storage: true storage: true
subresources: subresources:
status: {} status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -1,9 +1,11 @@
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.13.0 controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: webhookauthenticators.authentication.concierge.pinniped.dev name: webhookauthenticators.authentication.concierge.pinniped.dev
spec: spec:
group: authentication.concierge.pinniped.dev group: authentication.concierge.pinniped.dev
@ -69,15 +71,9 @@ spec:
description: Represents the observations of the authenticator's current description: Represents the observations of the authenticator's current
state. state.
items: items:
description: "Condition contains details for one aspect of the current description: Condition status of a resource (mirrored from the metav1.Condition
state of this API Resource. --- This struct is intended for direct type added in Kubernetes 1.19). In a future API version we can
use as an array at the field path .status.conditions. For example, switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties: properties:
lastTransitionTime: lastTransitionTime:
description: lastTransitionTime is the last time the condition description: lastTransitionTime is the last time the condition
@ -146,3 +142,9 @@ spec:
storage: true storage: true
subresources: subresources:
status: {} status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -1,9 +1,11 @@
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.13.0 controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: credentialissuers.config.concierge.pinniped.dev name: credentialissuers.config.concierge.pinniped.dev
spec: spec:
group: config.concierge.pinniped.dev group: config.concierge.pinniped.dev
@ -102,24 +104,6 @@ spec:
- None - None
type: string type: string
type: object type: object
tls:
description: "TLS contains information about how the Concierge
impersonation proxy should serve TLS. \n 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: required:
- mode - mode
- service - service
@ -255,3 +239,9 @@ spec:
storage: true storage: true
subresources: subresources:
status: {} status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

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 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
@ -12,14 +12,7 @@ apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: #@ data.values.namespace name: #@ data.values.namespace
labels: labels: #@ 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
#@ end #@ end
--- ---
apiVersion: v1 apiVersion: v1
@ -93,14 +86,8 @@ data:
imagePullSecrets: imagePullSecrets:
- image-pull-secret - image-pull-secret
(@ end @) (@ end @)
(@ if data.values.log_level or data.values.deprecated_log_format: @) (@ if data.values.log_level: @)
log: logLevel: (@= getAndValidateLogLevel() @)
(@ if data.values.log_level: @)
level: (@= getAndValidateLogLevel() @)
(@ end @)
(@ if data.values.deprecated_log_format: @)
format: (@= data.values.deprecated_log_format @)
(@ end @)
(@ end @) (@ end @)
--- ---
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "": #@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
@ -134,6 +121,8 @@ spec:
#! More recently added the more unique deploymentPodLabel() so Services can select these Pods more specifically #! 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. #! without accidentally selecting any other Deployment's Pods, especially the kube cert agent Deployment's Pods.
_: #@ template.replace(deploymentPodLabel()) _: #@ template.replace(deploymentPodLabel())
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ""
spec: spec:
securityContext: securityContext:
runAsUser: #@ data.values.run_as_user runAsUser: #@ data.values.run_as_user
@ -153,15 +142,6 @@ spec:
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
readOnlyRootFilesystem: true 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: resources:
requests: requests:
cpu: "100m" cpu: "100m"
@ -245,14 +225,9 @@ spec:
effect: NoSchedule effect: NoSchedule
- key: node-role.kubernetes.io/control-plane #! The new name for these nodes as of Kubernetes 1.24. - key: node-role.kubernetes.io/control-plane #! The new name for these nodes as of Kubernetes 1.24.
effect: NoSchedule effect: NoSchedule
- key: kubernetes.io/arch #! "system-cluster-critical" cannot be used outside the kube-system namespace until Kubernetes >= 1.17,
effect: NoSchedule #! so we skip setting this for now (see https://github.com/kubernetes/kubernetes/issues/60596).
operator: Equal #!priorityClassName: system-cluster-critical
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 #! This will help make sure our multiple pods run on different nodes, making
#! our deployment "more" "HA". #! our deployment "more" "HA".
affinity: affinity:

View File

@ -54,10 +54,6 @@ api_serving_certificate_renew_before_seconds: 2160000
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer #! Specify the verbosity of logging: info ("nice to know" information), debug (developer
#! information), trace (timing information), all (kitchen sink). #! 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. 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.
#! Specify the format of logging: json (for machine parsable logs) and text (for legacy klog formatted logs).
#! By default, when this value is left unset, logs are formatted in json.
#! This configuration is deprecated and will be removed in a future release at which point logs will always be formatted as json.
deprecated_log_format:
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_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 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,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 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
@ -65,26 +65,6 @@ spec:
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- local-user-authenticator - 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 apiVersion: v1
kind: Service kind: Service

View File

@ -1,9 +1,11 @@
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.13.0 controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: federationdomains.config.supervisor.pinniped.dev name: federationdomains.config.supervisor.pinniped.dev
spec: spec:
group: config.supervisor.pinniped.dev group: config.supervisor.pinniped.dev
@ -20,7 +22,7 @@ spec:
- jsonPath: .spec.issuer - jsonPath: .spec.issuer
name: Issuer name: Issuer
type: string type: string
- jsonPath: .status.phase - jsonPath: .status.status
name: Status name: Status
type: string type: string
- jsonPath: .metadata.creationTimestamp - jsonPath: .metadata.creationTimestamp
@ -46,264 +48,6 @@ spec:
spec: spec:
description: Spec of the OIDC provider. description: Spec of the OIDC provider.
properties: properties:
identityProviders:
description: "IdentityProviders is the list of identity providers
available for use by this FederationDomain. \n 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. \n 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.
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.
\n 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.
\n 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. \n 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.
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: issuer:
description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery
Metadata document, as well as the identifier that it will use for Metadata document, as well as the identifier that it will use for
@ -316,8 +60,8 @@ spec:
minLength: 1 minLength: 1
type: string type: string
tls: tls:
description: TLS specifies a secret which will contain Transport Layer description: TLS configures how this FederationDomain is served over
Security (TLS) configuration for the FederationDomain. Transport Layer Security (TLS).
properties: properties:
secretName: secretName:
description: "SecretName is an optional name of a Secret in the description: "SecretName is an optional name of a Secret in the
@ -333,13 +77,12 @@ spec:
so all issuers with the same DNS hostname must use the same so all issuers with the same DNS hostname must use the same
SecretName value even if they have different port numbers. \n SecretName value even if they have different port numbers. \n
SecretName is not required when you would like to use only the SecretName is not required when you would like to use only the
HTTP endpoints (e.g. when the HTTP listener is configured to HTTP endpoints (e.g. when terminating TLS at an Ingress). It
listen on loopback interfaces or UNIX domain sockets for traffic is also not required when you would like all requests to this
from a service mesh sidecar). It is also not required when you OIDC Provider's HTTPS endpoints to use the default TLS certificate,
would like all requests to this OIDC Provider's HTTPS endpoints which is configured elsewhere. \n When your Issuer URL's host
to use the default TLS certificate, which is configured elsewhere. is an IP address, then this field is ignored. SNI does not work
\n When your Issuer URL's host is an IP address, then this field for IP addresses."
is ignored. SNI does not work for IP addresses."
type: string type: string
type: object type: object
required: required:
@ -348,86 +91,14 @@ spec:
status: status:
description: Status of the OIDC provider. description: Status of the OIDC provider.
properties: properties:
conditions: lastUpdateTime:
description: Conditions represent the observations of an FederationDomain's description: LastUpdateTime holds the time at which the Status was
current state. last updated. It is a pointer to get around some undesirable behavior
items: with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811).
description: "Condition contains details for one aspect of the current format: date-time
state of this API Resource. --- This struct is intended for direct type: string
use as an array at the field path .status.conditions. For example, message:
\n type FooStatus struct{ // Represents the observations of a description: Message provides human-readable details about the Status.
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
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.
--- 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
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
type: string type: string
secrets: secrets:
description: Secrets contains information about this OIDC Provider's description: Secrets contains information about this OIDC Provider's
@ -444,7 +115,6 @@ spec:
TODO: Add other useful fields. apiVersion, kind, uid?' TODO: Add other useful fields. apiVersion, kind, uid?'
type: string type: string
type: object type: object
x-kubernetes-map-type: atomic
stateEncryptionKey: stateEncryptionKey:
description: StateSigningKey holds the name of the corev1.Secret description: StateSigningKey holds the name of the corev1.Secret
in which this OIDC Provider's key for encrypting state parameters in which this OIDC Provider's key for encrypting state parameters
@ -455,7 +125,6 @@ spec:
TODO: Add other useful fields. apiVersion, kind, uid?' TODO: Add other useful fields. apiVersion, kind, uid?'
type: string type: string
type: object type: object
x-kubernetes-map-type: atomic
stateSigningKey: stateSigningKey:
description: StateSigningKey holds the name of the corev1.Secret description: StateSigningKey holds the name of the corev1.Secret
in which this OIDC Provider's key for signing state parameters in which this OIDC Provider's key for signing state parameters
@ -466,7 +135,6 @@ spec:
TODO: Add other useful fields. apiVersion, kind, uid?' TODO: Add other useful fields. apiVersion, kind, uid?'
type: string type: string
type: object type: object
x-kubernetes-map-type: atomic
tokenSigningKey: tokenSigningKey:
description: TokenSigningKey holds the name of the corev1.Secret description: TokenSigningKey holds the name of the corev1.Secret
in which this OIDC Provider's key for signing tokens is stored. in which this OIDC Provider's key for signing tokens is stored.
@ -476,8 +144,16 @@ spec:
TODO: Add other useful fields. apiVersion, kind, uid?' TODO: Add other useful fields. apiVersion, kind, uid?'
type: string type: string
type: object type: object
x-kubernetes-map-type: atomic
type: object 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 type: object
required: required:
- spec - spec
@ -486,3 +162,9 @@ spec:
storage: true storage: true
subresources: subresources:
status: {} status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -1,220 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.13.0
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. \n 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.
\n 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
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. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
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.
--- 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
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 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
@ -10,7 +10,6 @@
#@ "namespace", #@ "namespace",
#@ "defaultResourceName", #@ "defaultResourceName",
#@ "defaultResourceNameWithSuffix", #@ "defaultResourceNameWithSuffix",
#@ "pinnipedDevAPIGroupWithPrefix",
#@ "getPinnipedConfigMapData", #@ "getPinnipedConfigMapData",
#@ "hasUnixNetworkEndpoint", #@ "hasUnixNetworkEndpoint",
#@ ) #@ )
@ -96,37 +95,12 @@ spec:
- /etc/config/pinniped.yaml - /etc/config/pinniped.yaml
securityContext: securityContext:
readOnlyRootFilesystem: true 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: resources:
requests: requests:
#! If OIDCClient CRs are being used, then the Supervisor needs enough CPU to run expensive bcrypt cpu: "100m"
#! 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
memory: "128Mi" memory: "128Mi"
limits: limits:
#! By declaring a CPU limit that is not equal to the CPU request value, the Supervisor will be classified cpu: "100m"
#! 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
memory: "128Mi" memory: "128Mi"
volumeMounts: volumeMounts:
- name: config-volume - name: config-volume
@ -141,6 +115,8 @@ spec:
readOnly: false #! writable to allow for socket use readOnly: false #! writable to allow for socket use
#@ end #@ end
ports: ports:
- containerPort: 8080
protocol: TCP
- containerPort: 8443 - containerPort: 8443
protocol: TCP protocol: TCP
env: env:
@ -190,15 +166,6 @@ spec:
- name: socket - name: socket
emptyDir: {} emptyDir: {}
#@ end #@ 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 #! This will help make sure our multiple pods run on different nodes, making
#! our deployment "more" "HA". #! our deployment "more" "HA".
affinity: affinity:
@ -209,37 +176,3 @@ spec:
labelSelector: labelSelector:
matchLabels: #@ deploymentPodLabel() matchLabels: #@ deploymentPodLabel()
topologyKey: kubernetes.io/hostname 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-2022 the Pinniped contributors. All Rights Reserved. #! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
@ -50,19 +50,11 @@ _: #@ template.replace(data.values.custom_labels)
#@ "apiGroupSuffix": data.values.api_group_suffix, #@ "apiGroupSuffix": data.values.api_group_suffix,
#@ "names": { #@ "names": {
#@ "defaultTLSCertificateSecret": defaultResourceNameWithSuffix("default-tls-certificate"), #@ "defaultTLSCertificateSecret": defaultResourceNameWithSuffix("default-tls-certificate"),
#@ "apiService": defaultResourceNameWithSuffix("api"),
#@ }, #@ },
#@ "labels": labels(), #@ "labels": labels(),
#@ "insecureAcceptExternalUnencryptedHttpRequests": data.values.deprecated_insecure_accept_external_unencrypted_http_requests
#@ } #@ }
#@ if data.values.log_level or data.values.deprecated_log_format:
#@ config["log"] = {}
#@ end
#@ if data.values.log_level: #@ if data.values.log_level:
#@ config["log"]["level"] = getAndValidateLogLevel() #@ config["logLevel"] = getAndValidateLogLevel()
#@ end
#@ if data.values.deprecated_log_format:
#@ config["log"]["format"] = data.values.deprecated_log_format
#@ end #@ end
#@ if data.values.endpoints: #@ if data.values.endpoints:
#@ config["endpoints"] = data.values.endpoints #@ config["endpoints"] = data.values.endpoints

View File

@ -1,9 +1,11 @@
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.13.0 controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev
spec: spec:
group: idp.supervisor.pinniped.dev group: idp.supervisor.pinniped.dev
@ -106,11 +108,10 @@ spec:
description: Filter is the ActiveDirectory search filter which description: Filter is the ActiveDirectory search filter which
should be applied when searching for groups for a user. The should be applied when searching for groups for a user. The
pattern "{}" must occur in the filter at least once and will pattern "{}" must occur in the filter at least once and will
be dynamically replaced by the value of an attribute of the be dynamically replaced by the dn (distinguished name) of the
user entry found as a result of the user search. Which attribute's user entry found as a result of the user search. E.g. "member={}"
value is used to replace the placeholder(s) depends on the value or "&(objectClass=groupOfNames)(member={})". For more information
of UserAttributeForFilter. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". about ActiveDirectory filters, see https://ldap.com/ldap-filters.
For more information about ActiveDirectory filters, see https://ldap.com/ldap-filters.
Note that the dn (distinguished name) is not an attribute of Note that the dn (distinguished name) is not an attribute of
an entry, so "dn={}" cannot be used. Optional. When not specified, 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:={})". the default will act as if the filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})".
@ -142,20 +143,6 @@ spec:
carefully read all release notes before upgrading to ensure carefully read all release notes before upgrading to ensure
that the meaning of this field has not changed." that the meaning of this field has not changed."
type: boolean 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 type: object
host: host:
description: 'Host is the hostname of this Active Directory identity description: 'Host is the hostname of this Active Directory identity
@ -231,15 +218,9 @@ spec:
description: Represents the observations of an identity provider's description: Represents the observations of an identity provider's
current state. current state.
items: items:
description: "Condition contains details for one aspect of the current description: Condition status of a resource (mirrored from the metav1.Condition
state of this API Resource. --- This struct is intended for direct type added in Kubernetes 1.19). In a future API version we can
use as an array at the field path .status.conditions. For example, switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties: properties:
lastTransitionTime: lastTransitionTime:
description: lastTransitionTime is the last time the condition description: lastTransitionTime is the last time the condition
@ -316,3 +297,9 @@ spec:
storage: true storage: true
subresources: subresources:
status: {} status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -1,9 +1,11 @@
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.13.0 controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: ldapidentityproviders.idp.supervisor.pinniped.dev name: ldapidentityproviders.idp.supervisor.pinniped.dev
spec: spec:
group: idp.supervisor.pinniped.dev group: idp.supervisor.pinniped.dev
@ -95,16 +97,15 @@ spec:
used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". 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 When not specified, no group search will be performed and authenticated
users will not belong to any groups from the LDAP provider. users will not belong to any groups from the LDAP provider.
Also, when not specified, the values of Filter, UserAttributeForFilter, Also, when not specified, the values of Filter and Attributes
Attributes, and SkipGroupRefresh are ignored. are ignored.
type: string type: string
filter: filter:
description: Filter is the LDAP search filter which should be description: Filter is the LDAP search filter which should be
applied when searching for groups for a user. The pattern "{}" applied when searching for groups for a user. The pattern "{}"
must occur in the filter at least once and will be dynamically must occur in the filter at least once and will be dynamically
replaced by the value of an attribute of the user entry found replaced by the dn (distinguished name) of the user entry found
as a result of the user search. Which attribute's value is used as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})".
to replace the placeholder(s) depends on the value of UserAttributeForFilter.
For more information about LDAP filters, see https://ldap.com/ldap-filters. For more information about LDAP filters, see https://ldap.com/ldap-filters.
Note that the dn (distinguished name) is not an attribute of Note that the dn (distinguished name) is not an attribute of
an entry, so "dn={}" cannot be used. Optional. When not specified, an entry, so "dn={}" cannot be used. Optional. When not specified,
@ -134,20 +135,6 @@ spec:
carefully read all release notes before upgrading to ensure carefully read all release notes before upgrading to ensure
that the meaning of this field has not changed." that the meaning of this field has not changed."
type: boolean 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 type: object
host: host:
description: 'Host is the hostname of this LDAP identity provider, description: 'Host is the hostname of this LDAP identity provider,
@ -228,15 +215,9 @@ spec:
description: Represents the observations of an identity provider's description: Represents the observations of an identity provider's
current state. current state.
items: items:
description: "Condition contains details for one aspect of the current description: Condition status of a resource (mirrored from the metav1.Condition
state of this API Resource. --- This struct is intended for direct type added in Kubernetes 1.19). In a future API version we can
use as an array at the field path .status.conditions. For example, switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties: properties:
lastTransitionTime: lastTransitionTime:
description: lastTransitionTime is the last time the condition description: lastTransitionTime is the last time the condition
@ -313,3 +294,9 @@ spec:
storage: true storage: true
subresources: subresources:
status: {} status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -1,9 +1,11 @@
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.13.0 controller-gen.kubebuilder.io/version: v0.4.0
creationTimestamp: null
name: oidcidentityproviders.idp.supervisor.pinniped.dev name: oidcidentityproviders.idp.supervisor.pinniped.dev
spec: spec:
group: idp.supervisor.pinniped.dev group: idp.supervisor.pinniped.dev
@ -184,24 +186,6 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: 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: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain
@ -258,15 +242,9 @@ spec:
description: Represents the observations of an identity provider's description: Represents the observations of an identity provider's
current state. current state.
items: items:
description: "Condition contains details for one aspect of the current description: Condition status of a resource (mirrored from the metav1.Condition
state of this API Resource. --- This struct is intended for direct type added in Kubernetes 1.19). In a future API version we can
use as an array at the field path .status.conditions. For example, switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties: properties:
lastTransitionTime: lastTransitionTime:
description: lastTransitionTime is the last time the condition description: lastTransitionTime is the last time the condition
@ -343,3 +321,9 @@ spec:
storage: true storage: true
subresources: subresources:
status: {} status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -1,4 +1,4 @@
#! Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. #! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
@ -24,14 +24,6 @@ rules:
- #@ pinnipedDevAPIGroupWithPrefix("config.supervisor") - #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
resources: [federationdomains/status] resources: [federationdomains/status]
verbs: [get, patch, update] 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: - apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
resources: [oidcidentityproviders] resources: [oidcidentityproviders]
@ -82,71 +74,3 @@ roleRef:
kind: Role kind: Role
name: #@ defaultResourceName() name: #@ defaultResourceName()
apiGroup: rbac.authorization.k8s.io 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 ]
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,24 +1,10 @@
#! Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. #! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
#@ load("@ytt:assert", "assert")
#@ load("helpers.lib.yaml", "labels", "deploymentPodLabel", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix") #@ load("helpers.lib.yaml", "labels", "deploymentPodLabel", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
#@ if hasattr(data.values, "service_http_nodeport_port"): #@ if data.values.service_http_nodeport_port or data.values.service_https_nodeport_port:
#@ assert.fail('value "service_http_nodeport_port" has been renamed to "deprecated_service_http_nodeport_port" and will be removed in a future release')
#@ end
#@ if hasattr(data.values, "service_http_nodeport_nodeport"):
#@ assert.fail('value "service_http_nodeport_nodeport" has been renamed to "deprecated_service_http_nodeport_nodeport" and will be removed in a future release')
#@ end
#@ if hasattr(data.values, "service_http_loadbalancer_port"):
#@ assert.fail('value "service_http_loadbalancer_port" has been renamed to "deprecated_service_http_loadbalancer_port" and will be removed in a future release')
#@ end
#@ if hasattr(data.values, "service_http_clusterip_port"):
#@ assert.fail('value "service_http_clusterip_port" has been renamed to "deprecated_service_http_clusterip_port" and will be removed in a future release')
#@ end
#@ if data.values.deprecated_service_http_nodeport_port or data.values.service_https_nodeport_port:
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@ -33,13 +19,13 @@ spec:
type: NodePort type: NodePort
selector: #@ deploymentPodLabel() selector: #@ deploymentPodLabel()
ports: ports:
#@ if data.values.deprecated_service_http_nodeport_port: #@ if data.values.service_http_nodeport_port:
- name: http - name: http
protocol: TCP protocol: TCP
port: #@ data.values.deprecated_service_http_nodeport_port port: #@ data.values.service_http_nodeport_port
targetPort: 8080 targetPort: 8080
#@ if data.values.deprecated_service_http_nodeport_nodeport: #@ if data.values.service_http_nodeport_nodeport:
nodePort: #@ data.values.deprecated_service_http_nodeport_nodeport nodePort: #@ data.values.service_http_nodeport_nodeport
#@ end #@ end
#@ end #@ end
#@ if data.values.service_https_nodeport_port: #@ if data.values.service_https_nodeport_port:
@ -53,7 +39,7 @@ spec:
#@ end #@ end
#@ end #@ end
#@ if data.values.deprecated_service_http_clusterip_port or data.values.service_https_clusterip_port: #@ if data.values.service_http_clusterip_port or data.values.service_https_clusterip_port:
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@ -68,10 +54,10 @@ spec:
type: ClusterIP type: ClusterIP
selector: #@ deploymentPodLabel() selector: #@ deploymentPodLabel()
ports: ports:
#@ if data.values.deprecated_service_http_clusterip_port: #@ if data.values.service_http_clusterip_port:
- name: http - name: http
protocol: TCP protocol: TCP
port: #@ data.values.deprecated_service_http_clusterip_port port: #@ data.values.service_http_clusterip_port
targetPort: 8080 targetPort: 8080
#@ end #@ end
#@ if data.values.service_https_clusterip_port: #@ if data.values.service_https_clusterip_port:
@ -82,7 +68,7 @@ spec:
#@ end #@ end
#@ end #@ end
#@ if data.values.deprecated_service_http_loadbalancer_port or data.values.service_https_loadbalancer_port: #@ if data.values.service_http_loadbalancer_port or data.values.service_https_loadbalancer_port:
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@ -100,10 +86,10 @@ spec:
loadBalancerIP: #@ data.values.service_loadbalancer_ip loadBalancerIP: #@ data.values.service_loadbalancer_ip
#@ end #@ end
ports: ports:
#@ if data.values.deprecated_service_http_loadbalancer_port: #@ if data.values.service_http_loadbalancer_port:
- name: http - name: http
protocol: TCP protocol: TCP
port: #@ data.values.deprecated_service_http_loadbalancer_port port: #@ data.values.service_http_loadbalancer_port
targetPort: 8080 targetPort: 8080
#@ end #@ end
#@ if data.values.service_https_loadbalancer_port: #@ if data.values.service_https_loadbalancer_port:

View File

@ -1,4 +1,4 @@
#! Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. #! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0 #! SPDX-License-Identifier: Apache-2.0
#@data/values #@data/values
@ -35,32 +35,27 @@ image_tag: latest
#! Optional. #! Optional.
image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}} image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}
#! Specify how to expose the Supervisor app's HTTPS port as a Service. #! 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. #! Typically you would set a value for only one of the following service types, for either HTTP or HTTPS depending on your needs.
#! Setting any of these values means that a Service of that type will be created. They are all optional. #! 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`. #! Note that all port numbers should be numbers (not strings), i.e. use ytt's `--data-value-yaml` instead of `--data-value`.
#! Several of these values have been deprecated and will be removed in a future release. Their names have been changed to service_http_nodeport_port: #! when specified, creates a NodePort Service with this `port` value, with port 8080 as its `targetPort`; e.g. 31234
#! mark them as deprecated and to make it obvious upon upgrade to anyone who was using them that they have been deprecated. service_http_nodeport_nodeport: #! the `nodePort` value of the NodePort Service, optional when `service_http_nodeport_port` is specified; e.g. 31234
deprecated_service_http_nodeport_port: #! will be removed in a future release; when specified, creates a NodePort Service with this `port` value, with port 8080 as its `targetPort`; 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
deprecated_service_http_nodeport_nodeport: #! will be removed in a future release; the `nodePort` value of the NodePort Service, optional when `deprecated_service_http_nodeport_port` is specified; e.g. 31234 service_http_clusterip_port: #! when specified, creates a ClusterIP Service with this `port` value, with port 8080 as its `targetPort`; e.g. 8443
deprecated_service_http_loadbalancer_port: #! will be removed in a future release; when specified, creates a LoadBalancer Service with this `port` value, with port 8080 as its `targetPort`; e.g. 8443
deprecated_service_http_clusterip_port: #! will be removed in a future release; 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_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_https_nodeport_port` is specified; 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_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 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. #! The `loadBalancerIP` value of the LoadBalancer Service.
#! Ignored unless service_https_loadbalancer_port is provided. #! Ignored unless service_http_loadbalancer_port and/or service_https_loadbalancer_port are provided.
#! Optional. #! Optional.
service_loadbalancer_ip: #! e.g. 1.2.3.4 service_loadbalancer_ip: #! e.g. 1.2.3.4
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer information), trace (timing information), #! Specify the verbosity of logging: info ("nice to know" information), debug (developer
#! or all (kitchen sink). Do not use trace or all on production systems, as credentials may get logged. #! 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. 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.
#! Specify the format of logging: json (for machine parsable logs) and text (for legacy klog formatted logs).
#! By default, when this value is left unset, logs are formatted in json.
#! This configuration is deprecated and will be removed in a future release at which point logs will always be formatted as json.
deprecated_log_format:
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_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 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
@ -79,17 +74,17 @@ api_group_suffix: pinniped.dev
https_proxy: #! e.g. http://proxy.example.com 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 no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.cluster.local" #! do not proxy Kubernetes endpoints
#! Control the HTTP and HTTPS listeners of the Supervisor. #! Control the https and http listeners of the Supervisor.
#! #!
#! The schema of this config is as follows: #! The schema of this config is as follows:
#! #!
#! endpoints: #! endpoints:
#! https: #! https:
#! network: tcp | unix | disabled #! network: tcp | unix | disabled
#! address: host:port when network=tcp or /pinniped_socket/socketfile.sock when network=unix #! address: interface:port when network=tcp or /pinniped_socket/socketfile.sock when network=unix
#! http: #! http:
#! network: same as above #! network: same as above
#! address: same as above, except that when network=tcp then the address is only allowed to bind to loopback interfaces #! address: same as above
#! #!
#! Setting network to disabled turns off that particular listener. #! 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 #! See https://pkg.go.dev/net#Listen and https://pkg.go.dev/net#Dial for a description of what can be
@ -103,31 +98,23 @@ no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.
#! network: tcp #! network: tcp
#! address: :8443 #! address: :8443
#! http: #! http:
#! network: disabled #! network: tcp
#! address: :8080
#! #!
#! These defaults mean: For HTTPS listening, bind to all interfaces using TCP on port 8443. #! These defaults mean: bind to all interfaces using TCP. Use port 8443 for https and 8080 for http.
#! Disable HTTP listening by default. #! The defaults will change over time. Users should explicitly set this value if they wish to avoid
#! any changes on upgrade.
#! #!
#! The HTTP listener can only be bound to loopback interfaces. This allows the listener to accept #! A future version of the Supervisor app may include a breaking change to adjust the default
#! traffic from within the pod, e.g. from a service mesh sidecar. The HTTP listener should not be #! behavior of the http listener to only listen on 127.0.0.1 (or perhaps even to be disabled).
#! 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 #! Binding the http listener to addresses other than 127.0.0.1 or ::1 is deprecated.
#! manifests. Changes to the HTTPS listener must be coordinated with the deployment health checks. #!
#! 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. #! Optional.
endpoints: endpoints:
#! Optionally override the validation on the endpoints.http value which checks that only loopback interfaces are used.
#! When deprecated_insecure_accept_external_unencrypted_http_requests is true, the HTTP listener is allowed to bind to any
#! interface, including interfaces that are listening for traffic from outside the pod. This value is being introduced
#! to ease the transition to the new loopback interface validation for the HTTP port for any users who need more time
#! to change their ingress strategy to avoid using plain HTTP into the Supervisor pods.
#! This value is immediately deprecated upon its introduction. It will be removed in some future release, at which time
#! traffic from outside the pod will need to be sent to the HTTPS listener instead, with no simple workaround available.
#! Allowed values are true (boolean), "true" (string), false (boolean), and "false" (string). The default is false.
#! Optional.
deprecated_insecure_accept_external_unencrypted_http_requests: false

View File

@ -1,4 +1,4 @@
#! Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. #! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:overlay", "overlay") #@ load("@ytt:overlay", "overlay")
@ -40,24 +40,3 @@ metadata:
name: #@ pinnipedDevAPIGroupWithPrefix("activedirectoryidentityproviders.idp.supervisor") name: #@ pinnipedDevAPIGroupWithPrefix("activedirectoryidentityproviders.idp.supervisor")
spec: spec:
group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"oidcclients.config.supervisor.pinniped.dev"}}), expects=1
---
metadata:
#@overlay/match missing_ok=True
labels: #@ labels()
name: #@ pinnipedDevAPIGroupWithPrefix("oidcclients.config.supervisor")
spec:
group: #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
versions:
#@overlay/match by=overlay.all, expects="1+"
- schema:
openAPIV3Schema:
#@overlay/match by=overlay.subset({"metadata":{"type":"object"}}), expects=1
properties:
metadata:
#@overlay/match missing_ok=True
properties:
name:
pattern: ^client\.oauth\.pinniped\.dev-
type: string

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-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -12,7 +12,7 @@ type JWTAuthenticatorStatus struct {
// +patchStrategy=merge // +patchStrategy=merge
// +listType=map // +listType=map
// +listMapKey=type // +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
} }
// Spec for configuring a JWT authenticator. // Spec for configuring a JWT authenticator.

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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -12,7 +12,7 @@ type WebhookAuthenticatorStatus struct {
// +patchStrategy=merge // +patchStrategy=merge
// +listType=map // +listType=map
// +listMapKey=type // +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
} }
// Spec for configuring a webhook authenticator. // Spec for configuring a webhook authenticator.

View File

@ -1,7 +1,7 @@
//go:build !ignore_autogenerated //go:build !ignore_autogenerated
// +build !ignore_autogenerated // +build !ignore_autogenerated
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Code generated by deepcopy-gen. DO NOT EDIT. // Code generated by deepcopy-gen. DO NOT EDIT.
@ -9,10 +9,26 @@
package v1alpha1 package v1alpha1
import ( import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Condition) DeepCopyInto(out *Condition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
func (in *Condition) DeepCopy() *Condition {
if in == nil {
return nil
}
out := new(Condition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) {
*out = *in *out = *in
@ -101,7 +117,7 @@ func (in *JWTAuthenticatorStatus) DeepCopyInto(out *JWTAuthenticatorStatus) {
*out = *in *out = *in
if in.Conditions != nil { if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in)) *out = make([]Condition, len(*in))
for i := range *in { for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
@ -238,7 +254,7 @@ func (in *WebhookAuthenticatorStatus) DeepCopyInto(out *WebhookAuthenticatorStat
*out = *in *out = *in
if in.Conditions != nil { if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in)) *out = make([]Condition, len(*in))
for i := range *in { for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }

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 // SPDX-License-Identifier: Apache-2.0
package v1alpha1 package v1alpha1
@ -80,28 +80,6 @@ const (
ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") 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. // ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy.
type ImpersonationProxySpec struct { type ImpersonationProxySpec struct {
// Mode configures whether the impersonation proxy should be started: // Mode configures whether the impersonation proxy should be started:
@ -122,13 +100,6 @@ type ImpersonationProxySpec struct {
// //
// +optional // +optional
ExternalEndpoint string `json:"externalEndpoint,omitempty"` 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. // ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy.

View File

@ -1,7 +1,7 @@
//go:build !ignore_autogenerated //go:build !ignore_autogenerated
// +build !ignore_autogenerated // +build !ignore_autogenerated
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Code generated by deepcopy-gen. DO NOT EDIT. // Code generated by deepcopy-gen. DO NOT EDIT.
@ -229,11 +229,6 @@ func (in *ImpersonationProxyServiceSpec) DeepCopy() *ImpersonationProxyServiceSp
func (in *ImpersonationProxySpec) DeepCopyInto(out *ImpersonationProxySpec) { func (in *ImpersonationProxySpec) DeepCopyInto(out *ImpersonationProxySpec) {
*out = *in *out = *in
in.Service.DeepCopyInto(&out.Service) in.Service.DeepCopyInto(&out.Service)
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(ImpersonationProxyTLSSpec)
**out = **in
}
return return
} }
@ -247,22 +242,6 @@ func (in *ImpersonationProxySpec) DeepCopy() *ImpersonationProxySpec {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImpersonationProxyTLSSpec) DeepCopyInto(out *ImpersonationProxyTLSSpec) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyTLSSpec.
func (in *ImpersonationProxyTLSSpec) DeepCopy() *ImpersonationProxyTLSSpec {
if in == nil {
return nil
}
out := new(ImpersonationProxyTLSSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) { func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) {
*out = *in *out = *in

View File

@ -17,13 +17,11 @@ type WhoAmIRequest struct {
Status WhoAmIRequestStatus Status WhoAmIRequestStatus
} }
// Spec is always empty for a WhoAmIRequest.
type WhoAmIRequestSpec struct { type WhoAmIRequestSpec struct {
// empty for now but we may add some config here in the future // 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 // 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 { type WhoAmIRequestStatus struct {
// The current authenticated user, exactly as Kubernetes understands it. // The current authenticated user, exactly as Kubernetes understands it.
KubernetesUserInfo KubernetesUserInfo KubernetesUserInfo KubernetesUserInfo
@ -37,6 +35,6 @@ type WhoAmIRequestList struct {
metav1.TypeMeta metav1.TypeMeta
metav1.ListMeta metav1.ListMeta
// Items is a list of WhoAmIRequest. // Items is a list of WhoAmIRequest
Items []WhoAmIRequest Items []WhoAmIRequest
} }

View File

@ -3,7 +3,7 @@
// +k8s:openapi-gen=true // +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package // +k8s:deepcopy-gen=package
// +k8s:conversion-gen=go.pinniped.dev/generated/1.25/apis/concierge/identity // +k8s:conversion-gen=go.pinniped.dev/generated/1.17/apis/concierge/identity
// +k8s:defaulter-gen=TypeMeta // +k8s:defaulter-gen=TypeMeta
// +groupName=identity.concierge.pinniped.dev // +groupName=identity.concierge.pinniped.dev

View File

@ -20,13 +20,11 @@ type WhoAmIRequest struct {
Status WhoAmIRequestStatus `json:"status,omitempty"` Status WhoAmIRequestStatus `json:"status,omitempty"`
} }
// Spec is always empty for a WhoAmIRequest.
type WhoAmIRequestSpec struct { type WhoAmIRequestSpec struct {
// empty for now but we may add some config here in the future // 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 // 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 { type WhoAmIRequestStatus struct {
// The current authenticated user, exactly as Kubernetes understands it. // The current authenticated user, exactly as Kubernetes understands it.
KubernetesUserInfo KubernetesUserInfo `json:"kubernetesUserInfo"` KubernetesUserInfo KubernetesUserInfo `json:"kubernetesUserInfo"`
@ -40,6 +38,6 @@ type WhoAmIRequestList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"` metav1.ListMeta `json:"metadata,omitempty"`
// Items is a list of WhoAmIRequest. // Items is a list of WhoAmIRequest
Items []WhoAmIRequest `json:"items"` Items []WhoAmIRequest `json:"items"`
} }

View File

@ -1,7 +1,7 @@
//go:build !ignore_autogenerated //go:build !ignore_autogenerated
// +build !ignore_autogenerated // +build !ignore_autogenerated
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Code generated by conversion-gen. DO NOT EDIT. // Code generated by conversion-gen. DO NOT EDIT.
@ -11,7 +11,7 @@ package v1alpha1
import ( import (
unsafe "unsafe" unsafe "unsafe"
identity "go.pinniped.dev/generated/1.26/apis/concierge/identity" identity "go.pinniped.dev/generated/1.17/apis/concierge/identity"
conversion "k8s.io/apimachinery/pkg/conversion" conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )

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