commit
d75bd8bf2a
5
Godeps/Godeps.json
generated
5
Godeps/Godeps.json
generated
@ -89,6 +89,11 @@
|
||||
"ImportPath": "github.com/daviddengcn/go-colortext",
|
||||
"Rev": "b5c0891944c2f150ccc9d02aecf51b76c14c2948"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/dgrijalva/jwt-go",
|
||||
"Comment": "v2.2.0-23-g5ca8014",
|
||||
"Rev": "5ca80149b9d3f8b863af0e2bb6742e608603bd99"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/docker/docker/pkg/archive",
|
||||
"Comment": "v1.4.1-1714-ged66853",
|
||||
|
4
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/.gitignore
generated
vendored
Normal file
4
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/.gitignore
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
bin
|
||||
|
||||
|
8
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/LICENSE
generated
vendored
Normal file
8
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/LICENSE
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
Copyright (c) 2012 Dave Grijalva
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
61
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md
generated
vendored
Normal file
61
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md
generated
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-jones-json-web-token.html)
|
||||
|
||||
**NOTICE:** A vulnerability in JWT was [recently published](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). As this library doesn't force users to validate the `alg` is what they expected, it's possible your usage is effected. There will be an update soon to remedy this, and it will likey require backwards-incompatible changes to the API. In the short term, please make sure your implementation verifies the `alg` is what you expect.
|
||||
|
||||
## What the heck is a JWT?
|
||||
|
||||
In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way.
|
||||
|
||||
The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used.
|
||||
|
||||
The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [the RFC](http://self-issued.info/docs/draft-jones-json-web-token.html) for information about reserved keys and the proper way to add your own.
|
||||
|
||||
## What's in the box?
|
||||
|
||||
This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are RSA256 and HMAC SHA256, though hooks are present for adding your own.
|
||||
|
||||
## Parse and Verify
|
||||
|
||||
Parsing and verifying tokens is pretty straight forward. You pass in the token and a function for looking up the key. This is done as a callback since you may need to parse the token to find out what signing method and key was used.
|
||||
|
||||
```go
|
||||
token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) {
|
||||
// Don't forget to validate the alg is what you expect:
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return myLookupKey(token.Header["kid"])
|
||||
})
|
||||
|
||||
if err == nil && token.Valid {
|
||||
deliverGoodness("!")
|
||||
} else {
|
||||
deliverUtterRejection(":(")
|
||||
}
|
||||
```
|
||||
|
||||
## Create a token
|
||||
|
||||
```go
|
||||
// Create the token
|
||||
token := jwt.New(jwt.SigningMethodHS256)
|
||||
// Set some claims
|
||||
token.Claims["foo"] = "bar"
|
||||
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||
// Sign and get the complete encoded token as a string
|
||||
tokenString, err := token.SignedString(mySigningKey)
|
||||
```
|
||||
|
||||
## Project Status & Versioning
|
||||
|
||||
This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason).
|
||||
|
||||
This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases).
|
||||
|
||||
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning.
|
||||
|
||||
## More
|
||||
|
||||
Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go).
|
||||
|
||||
The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. For a more http centric example, see [this gist](https://gist.github.com/cryptix/45c33ecf0ae54828e63b).
|
54
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md
generated
vendored
Normal file
54
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md
generated
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
## `jwt-go` Version History
|
||||
|
||||
#### 2.2.0
|
||||
|
||||
* Gracefully handle a `nil` `Keyfunc` being passed to `Parse`. Result will now be the parsed token and an error, instead of a panic.
|
||||
|
||||
#### 2.1.0
|
||||
|
||||
Backwards compatible API change that was missed in 2.0.0.
|
||||
|
||||
* The `SignedString` method on `Token` now takes `interface{}` instead of `[]byte`
|
||||
|
||||
#### 2.0.0
|
||||
|
||||
There were two major reasons for breaking backwards compatibility with this update. The first was a refactor required to expand the width of the RSA and HMAC-SHA signing implementations. There will likely be no required code changes to support this change.
|
||||
|
||||
The second update, while unfortunately requiring a small change in integration, is required to open up this library to other signing methods. Not all keys used for all signing methods have a single standard on-disk representation. Requiring `[]byte` as the type for all keys proved too limiting. Additionally, this implementation allows for pre-parsed tokens to be reused, which might matter in an application that parses a high volume of tokens with a small set of keys. Backwards compatibilty has been maintained for passing `[]byte` to the RSA signing methods, but they will also accept `*rsa.PublicKey` and `*rsa.PrivateKey`.
|
||||
|
||||
It is likely the only integration change required here will be to change `func(t *jwt.Token) ([]byte, error)` to `func(t *jwt.Token) (interface{}, error)` when calling `Parse`.
|
||||
|
||||
* **Compatibility Breaking Changes**
|
||||
* `SigningMethodHS256` is now `*SigningMethodHMAC` instead of `type struct`
|
||||
* `SigningMethodRS256` is now `*SigningMethodRSA` instead of `type struct`
|
||||
* `KeyFunc` now returns `interface{}` instead of `[]byte`
|
||||
* `SigningMethod.Sign` now takes `interface{}` instead of `[]byte` for the key
|
||||
* `SigningMethod.Verify` now takes `interface{}` instead of `[]byte` for the key
|
||||
* Renamed type `SigningMethodHS256` to `SigningMethodHMAC`. Specific sizes are now just instances of this type.
|
||||
* Added public package global `SigningMethodHS256`
|
||||
* Added public package global `SigningMethodHS384`
|
||||
* Added public package global `SigningMethodHS512`
|
||||
* Renamed type `SigningMethodRS256` to `SigningMethodRSA`. Specific sizes are now just instances of this type.
|
||||
* Added public package global `SigningMethodRS256`
|
||||
* Added public package global `SigningMethodRS384`
|
||||
* Added public package global `SigningMethodRS512`
|
||||
* Moved sample private key for HMAC tests from an inline value to a file on disk. Value is unchanged.
|
||||
* Refactored the RSA implementation to be easier to read
|
||||
* Exposed helper methods `ParseRSAPrivateKeyFromPEM` and `ParseRSAPublicKeyFromPEM`
|
||||
|
||||
#### 1.0.2
|
||||
|
||||
* Fixed bug in parsing public keys from certificates
|
||||
* Added more tests around the parsing of keys for RS256
|
||||
* Code refactoring in RS256 implementation. No functional changes
|
||||
|
||||
#### 1.0.1
|
||||
|
||||
* Fixed panic if RS256 signing method was passed an invalid key
|
||||
|
||||
#### 1.0.0
|
||||
|
||||
* First versioned release
|
||||
* API stabilized
|
||||
* Supports creating, signing, parsing, and validating JWT tokens
|
||||
* Supports RS256 and HS256 signing methods
|
186
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/cmd/jwt/app.go
generated
vendored
Normal file
186
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/cmd/jwt/app.go
generated
vendored
Normal file
@ -0,0 +1,186 @@
|
||||
// A useful example app. You can use this to debug your tokens on the command line.
|
||||
// This is also a great place to look at how you might use this library.
|
||||
//
|
||||
// Example usage:
|
||||
// The following will create and sign a token, then verify it and output the original claims.
|
||||
// echo {\"foo\":\"bar\"} | bin/jwt -key test/sample_key -alg RS256 -sign - | bin/jwt -key test/sample_key.pub -verify -
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
var (
|
||||
// Options
|
||||
flagAlg = flag.String("alg", "", "signing algorithm identifier")
|
||||
flagKey = flag.String("key", "", "path to key file or '-' to read from stdin")
|
||||
flagCompact = flag.Bool("compact", false, "output compact JSON")
|
||||
flagDebug = flag.Bool("debug", false, "print out all kinds of debug data")
|
||||
|
||||
// Modes - exactly one of these is required
|
||||
flagSign = flag.String("sign", "", "path to claims object to sign or '-' to read from stdin")
|
||||
flagVerify = flag.String("verify", "", "path to JWT token to verify or '-' to read from stdin")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Usage message if you ask for -help or if you mess up inputs.
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " One of the following flags is required: sign, verify\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
// Parse command line options
|
||||
flag.Parse()
|
||||
|
||||
// Do the thing. If something goes wrong, print error to stderr
|
||||
// and exit with a non-zero status code
|
||||
if err := start(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out which thing to do and then do that
|
||||
func start() error {
|
||||
if *flagSign != "" {
|
||||
return signToken()
|
||||
} else if *flagVerify != "" {
|
||||
return verifyToken()
|
||||
} else {
|
||||
flag.Usage()
|
||||
return fmt.Errorf("None of the required flags are present. What do you want me to do?")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper func: Read input from specified file or stdin
|
||||
func loadData(p string) ([]byte, error) {
|
||||
if p == "" {
|
||||
return nil, fmt.Errorf("No path specified")
|
||||
}
|
||||
|
||||
var rdr io.Reader
|
||||
if p == "-" {
|
||||
rdr = os.Stdin
|
||||
} else {
|
||||
if f, err := os.Open(p); err == nil {
|
||||
rdr = f
|
||||
defer f.Close()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ioutil.ReadAll(rdr)
|
||||
}
|
||||
|
||||
// Print a json object in accordance with the prophecy (or the command line options)
|
||||
func printJSON(j interface{}) error {
|
||||
var out []byte
|
||||
var err error
|
||||
|
||||
if *flagCompact == false {
|
||||
out, err = json.MarshalIndent(j, "", " ")
|
||||
} else {
|
||||
out, err = json.Marshal(j)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify a token and output the claims. This is a great example
|
||||
// of how to verify and view a token.
|
||||
func verifyToken() error {
|
||||
// get the token
|
||||
tokData, err := loadData(*flagVerify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't read token: %v", err)
|
||||
}
|
||||
|
||||
// trim possible whitespace from token
|
||||
tokData = regexp.MustCompile(`\s*$`).ReplaceAll(tokData, []byte{})
|
||||
if *flagDebug {
|
||||
fmt.Fprintf(os.Stderr, "Token len: %v bytes\n", len(tokData))
|
||||
}
|
||||
|
||||
// Parse the token. Load the key from command line option
|
||||
token, err := jwt.Parse(string(tokData), func(t *jwt.Token) (interface{}, error) {
|
||||
return loadData(*flagKey)
|
||||
})
|
||||
|
||||
// Print some debug data
|
||||
if *flagDebug && token != nil {
|
||||
fmt.Fprintf(os.Stderr, "Header:\n%v\n", token.Header)
|
||||
fmt.Fprintf(os.Stderr, "Claims:\n%v\n", token.Claims)
|
||||
}
|
||||
|
||||
// Print an error if we can't parse for some reason
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't parse token: %v", err)
|
||||
}
|
||||
|
||||
// Is token invalid?
|
||||
if !token.Valid {
|
||||
return fmt.Errorf("Token is invalid")
|
||||
}
|
||||
|
||||
// Print the token details
|
||||
if err := printJSON(token.Claims); err != nil {
|
||||
return fmt.Errorf("Failed to output claims: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create, sign, and output a token. This is a great, simple example of
|
||||
// how to use this library to create and sign a token.
|
||||
func signToken() error {
|
||||
// get the token data from command line arguments
|
||||
tokData, err := loadData(*flagSign)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't read token: %v", err)
|
||||
} else if *flagDebug {
|
||||
fmt.Fprintf(os.Stderr, "Token: %v bytes", len(tokData))
|
||||
}
|
||||
|
||||
// parse the JSON of the claims
|
||||
var claims map[string]interface{}
|
||||
if err := json.Unmarshal(tokData, &claims); err != nil {
|
||||
return fmt.Errorf("Couldn't parse claims JSON: %v", err)
|
||||
}
|
||||
|
||||
// get the key
|
||||
keyData, err := loadData(*flagKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't read key: %v", err)
|
||||
}
|
||||
|
||||
// get the signing alg
|
||||
alg := jwt.GetSigningMethod(*flagAlg)
|
||||
if alg == nil {
|
||||
return fmt.Errorf("Couldn't find signing method: %v", *flagAlg)
|
||||
}
|
||||
|
||||
// create a new token
|
||||
token := jwt.New(alg)
|
||||
token.Claims = claims
|
||||
|
||||
if out, err := token.SignedString(keyData); err == nil {
|
||||
fmt.Println(out)
|
||||
} else {
|
||||
return fmt.Errorf("Error signing token: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
4
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/doc.go
generated
vendored
Normal file
4
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/doc.go
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// Package jwt is a Go implementation of JSON Web Tokens: http://self-issued.info/docs/draft-jones-json-web-token.html
|
||||
//
|
||||
// See README.md for more info.
|
||||
package jwt
|
43
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/errors.go
generated
vendored
Normal file
43
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/errors.go
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Error constants
|
||||
var (
|
||||
ErrInvalidKey = errors.New("key is invalid or of invalid type")
|
||||
ErrHashUnavailable = errors.New("the requested hash function is unavailable")
|
||||
ErrNoTokenInRequest = errors.New("no token present in request")
|
||||
)
|
||||
|
||||
// The errors that might occur when parsing and validating a token
|
||||
const (
|
||||
ValidationErrorMalformed uint32 = 1 << iota // Token is malformed
|
||||
ValidationErrorUnverifiable // Token could not be verified because of signing problems
|
||||
ValidationErrorSignatureInvalid // Signature validation failed
|
||||
ValidationErrorExpired // Exp validation failed
|
||||
ValidationErrorNotValidYet // NBF validation failed
|
||||
)
|
||||
|
||||
// The error from Parse if token is not valid
|
||||
type ValidationError struct {
|
||||
err string
|
||||
Errors uint32 // bitfield. see ValidationError... constants
|
||||
}
|
||||
|
||||
// Validation error is an error type
|
||||
func (e ValidationError) Error() string {
|
||||
if e.err == "" {
|
||||
return "token is invalid"
|
||||
}
|
||||
return e.err
|
||||
}
|
||||
|
||||
// No errors
|
||||
func (e *ValidationError) valid() bool {
|
||||
if e.Errors > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
52
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go
generated
vendored
Normal file
52
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go
generated
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, error)) {
|
||||
token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) {
|
||||
return myLookupKey(token.Header["kid"])
|
||||
})
|
||||
|
||||
if err == nil && token.Valid {
|
||||
fmt.Println("Your token is valid. I like your style.")
|
||||
} else {
|
||||
fmt.Println("This token is terrible! I cannot accept this.")
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleNew(mySigningKey []byte) (string, error) {
|
||||
// Create the token
|
||||
token := jwt.New(jwt.SigningMethodHS256)
|
||||
// Set some claims
|
||||
token.Claims["foo"] = "bar"
|
||||
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||
// Sign and get the complete encoded token as a string
|
||||
tokenString, err := token.SignedString(mySigningKey)
|
||||
return tokenString, err
|
||||
}
|
||||
|
||||
func ExampleParse_errorChecking(myToken string, myLookupKey func(interface{}) (interface{}, error)) {
|
||||
token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) {
|
||||
return myLookupKey(token.Header["kid"])
|
||||
})
|
||||
|
||||
if token.Valid {
|
||||
fmt.Println("You look nice today")
|
||||
} else if ve, ok := err.(*jwt.ValidationError); ok {
|
||||
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
|
||||
fmt.Println("That's not even a token")
|
||||
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
|
||||
// Token is either expired or not active yet
|
||||
fmt.Println("Timing is everything")
|
||||
} else {
|
||||
fmt.Println("Couldn't handle this token:", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Couldn't handle this token:", err)
|
||||
}
|
||||
|
||||
}
|
84
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac.go
generated
vendored
Normal file
84
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac.go
generated
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/hmac"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Implements the HMAC-SHA family of signing methods signing methods
|
||||
type SigningMethodHMAC struct {
|
||||
Name string
|
||||
Hash crypto.Hash
|
||||
}
|
||||
|
||||
// Specific instances for HS256 and company
|
||||
var (
|
||||
SigningMethodHS256 *SigningMethodHMAC
|
||||
SigningMethodHS384 *SigningMethodHMAC
|
||||
SigningMethodHS512 *SigningMethodHMAC
|
||||
ErrSignatureInvalid = errors.New("signature is invalid")
|
||||
)
|
||||
|
||||
func init() {
|
||||
// HS256
|
||||
SigningMethodHS256 = &SigningMethodHMAC{"HS256", crypto.SHA256}
|
||||
RegisterSigningMethod(SigningMethodHS256.Alg(), func() SigningMethod {
|
||||
return SigningMethodHS256
|
||||
})
|
||||
|
||||
// HS384
|
||||
SigningMethodHS384 = &SigningMethodHMAC{"HS384", crypto.SHA384}
|
||||
RegisterSigningMethod(SigningMethodHS384.Alg(), func() SigningMethod {
|
||||
return SigningMethodHS384
|
||||
})
|
||||
|
||||
// HS512
|
||||
SigningMethodHS512 = &SigningMethodHMAC{"HS512", crypto.SHA512}
|
||||
RegisterSigningMethod(SigningMethodHS512.Alg(), func() SigningMethod {
|
||||
return SigningMethodHS512
|
||||
})
|
||||
}
|
||||
|
||||
func (m *SigningMethodHMAC) Alg() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error {
|
||||
if keyBytes, ok := key.([]byte); ok {
|
||||
var sig []byte
|
||||
var err error
|
||||
if sig, err = DecodeSegment(signature); err == nil {
|
||||
if !m.Hash.Available() {
|
||||
return ErrHashUnavailable
|
||||
}
|
||||
|
||||
hasher := hmac.New(m.Hash.New, keyBytes)
|
||||
hasher.Write([]byte(signingString))
|
||||
|
||||
if !hmac.Equal(sig, hasher.Sum(nil)) {
|
||||
err = ErrSignatureInvalid
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return ErrInvalidKey
|
||||
}
|
||||
|
||||
// Implements the Sign method from SigningMethod for this signing method.
|
||||
// Key must be []byte
|
||||
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
|
||||
if keyBytes, ok := key.([]byte); ok {
|
||||
if !m.Hash.Available() {
|
||||
return "", ErrHashUnavailable
|
||||
}
|
||||
|
||||
hasher := hmac.New(m.Hash.New, keyBytes)
|
||||
hasher.Write([]byte(signingString))
|
||||
|
||||
return EncodeSegment(hasher.Sum(nil)), nil
|
||||
}
|
||||
|
||||
return "", ErrInvalidKey
|
||||
}
|
91
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac_test.go
generated
vendored
Normal file
91
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac_test.go
generated
vendored
Normal file
@ -0,0 +1,91 @@
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var hmacTestData = []struct {
|
||||
name string
|
||||
tokenString string
|
||||
alg string
|
||||
claims map[string]interface{}
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
"web sample",
|
||||
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
||||
"HS256",
|
||||
map[string]interface{}{"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"HS384",
|
||||
"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJleHAiOjEuMzAwODE5MzhlKzA5LCJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiam9lIn0.KWZEuOD5lbBxZ34g7F-SlVLAQ_r5KApWNWlZIIMyQVz5Zs58a7XdNzj5_0EcNoOy",
|
||||
"HS384",
|
||||
map[string]interface{}{"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"HS512",
|
||||
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEuMzAwODE5MzhlKzA5LCJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiam9lIn0.CN7YijRX6Aw1n2jyI2Id1w90ja-DEMYiWixhYCyHnrZ1VfJRaFQz1bEbjjA5Fn4CLYaUG432dEYmSbS4Saokmw",
|
||||
"HS512",
|
||||
map[string]interface{}{"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"web sample: invalid",
|
||||
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXo",
|
||||
"HS256",
|
||||
map[string]interface{}{"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
// Sample data from http://tools.ietf.org/html/draft-jones-json-web-signature-04#appendix-A.1
|
||||
var hmacTestKey, _ = ioutil.ReadFile("test/hmacTestKey")
|
||||
|
||||
func TestHMACVerify(t *testing.T) {
|
||||
for _, data := range hmacTestData {
|
||||
parts := strings.Split(data.tokenString, ".")
|
||||
|
||||
method := jwt.GetSigningMethod(data.alg)
|
||||
err := method.Verify(strings.Join(parts[0:2], "."), parts[2], hmacTestKey)
|
||||
if data.valid && err != nil {
|
||||
t.Errorf("[%v] Error while verifying key: %v", data.name, err)
|
||||
}
|
||||
if !data.valid && err == nil {
|
||||
t.Errorf("[%v] Invalid key passed validation", data.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHMACSign(t *testing.T) {
|
||||
for _, data := range hmacTestData {
|
||||
if data.valid {
|
||||
parts := strings.Split(data.tokenString, ".")
|
||||
method := jwt.GetSigningMethod(data.alg)
|
||||
sig, err := method.Sign(strings.Join(parts[0:2], "."), hmacTestKey)
|
||||
if err != nil {
|
||||
t.Errorf("[%v] Error signing token: %v", data.name, err)
|
||||
}
|
||||
if sig != parts[2] {
|
||||
t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", data.name, sig, parts[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHS256Signing(b *testing.B) {
|
||||
benchmarkSigning(b, jwt.SigningMethodHS256, hmacTestKey)
|
||||
}
|
||||
|
||||
func BenchmarkHS384Signing(b *testing.B) {
|
||||
benchmarkSigning(b, jwt.SigningMethodHS384, hmacTestKey)
|
||||
}
|
||||
|
||||
func BenchmarkHS512Signing(b *testing.B) {
|
||||
benchmarkSigning(b, jwt.SigningMethodHS512, hmacTestKey)
|
||||
}
|
198
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt.go
generated
vendored
Normal file
198
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt.go
generated
vendored
Normal file
@ -0,0 +1,198 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time).
|
||||
// You can override it to use another time value. This is useful for testing or if your
|
||||
// server uses a different time zone than your tokens.
|
||||
var TimeFunc = time.Now
|
||||
|
||||
// Parse methods use this callback function to supply
|
||||
// the key for verification. The function receives the parsed,
|
||||
// but unverified Token. This allows you to use propries in the
|
||||
// Header of the token (such as `kid`) to identify which key to use.
|
||||
type Keyfunc func(*Token) (interface{}, error)
|
||||
|
||||
// A JWT Token. Different fields will be used depending on whether you're
|
||||
// creating or parsing/verifying a token.
|
||||
type Token struct {
|
||||
Raw string // The raw token. Populated when you Parse a token
|
||||
Method SigningMethod // The signing method used or to be used
|
||||
Header map[string]interface{} // The first segment of the token
|
||||
Claims map[string]interface{} // The second segment of the token
|
||||
Signature string // The third segment of the token. Populated when you Parse a token
|
||||
Valid bool // Is the token valid? Populated when you Parse/Verify a token
|
||||
}
|
||||
|
||||
// Create a new Token. Takes a signing method
|
||||
func New(method SigningMethod) *Token {
|
||||
return &Token{
|
||||
Header: map[string]interface{}{
|
||||
"typ": "JWT",
|
||||
"alg": method.Alg(),
|
||||
},
|
||||
Claims: make(map[string]interface{}),
|
||||
Method: method,
|
||||
}
|
||||
}
|
||||
|
||||
// Get the complete, signed token
|
||||
func (t *Token) SignedString(key interface{}) (string, error) {
|
||||
var sig, sstr string
|
||||
var err error
|
||||
if sstr, err = t.SigningString(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if sig, err = t.Method.Sign(sstr, key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Join([]string{sstr, sig}, "."), nil
|
||||
}
|
||||
|
||||
// Generate the signing string. This is the
|
||||
// most expensive part of the whole deal. Unless you
|
||||
// need this for something special, just go straight for
|
||||
// the SignedString.
|
||||
func (t *Token) SigningString() (string, error) {
|
||||
var err error
|
||||
parts := make([]string, 2)
|
||||
for i, _ := range parts {
|
||||
var source map[string]interface{}
|
||||
if i == 0 {
|
||||
source = t.Header
|
||||
} else {
|
||||
source = t.Claims
|
||||
}
|
||||
|
||||
var jsonValue []byte
|
||||
if jsonValue, err = json.Marshal(source); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parts[i] = EncodeSegment(jsonValue)
|
||||
}
|
||||
return strings.Join(parts, "."), nil
|
||||
}
|
||||
|
||||
// Parse, validate, and return a token.
|
||||
// keyFunc will receive the parsed token and should return the key for validating.
|
||||
// If everything is kosher, err will be nil
|
||||
func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed}
|
||||
}
|
||||
|
||||
var err error
|
||||
token := &Token{Raw: tokenString}
|
||||
// parse Header
|
||||
var headerBytes []byte
|
||||
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
|
||||
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
|
||||
}
|
||||
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
|
||||
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
|
||||
}
|
||||
|
||||
// parse Claims
|
||||
var claimBytes []byte
|
||||
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
|
||||
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
|
||||
}
|
||||
if err = json.Unmarshal(claimBytes, &token.Claims); err != nil {
|
||||
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
|
||||
}
|
||||
|
||||
// Lookup signature method
|
||||
if method, ok := token.Header["alg"].(string); ok {
|
||||
if token.Method = GetSigningMethod(method); token.Method == nil {
|
||||
return token, &ValidationError{err: "signing method (alg) is unavailable.", Errors: ValidationErrorUnverifiable}
|
||||
}
|
||||
} else {
|
||||
return token, &ValidationError{err: "signing method (alg) is unspecified.", Errors: ValidationErrorUnverifiable}
|
||||
}
|
||||
|
||||
// Lookup key
|
||||
var key interface{}
|
||||
if keyFunc == nil {
|
||||
// keyFunc was not provided. short circuiting validation
|
||||
return token, &ValidationError{err: "no Keyfunc was provided.", Errors: ValidationErrorUnverifiable}
|
||||
}
|
||||
if key, err = keyFunc(token); err != nil {
|
||||
// keyFunc returned an error
|
||||
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorUnverifiable}
|
||||
}
|
||||
|
||||
// Check expiration times
|
||||
vErr := &ValidationError{}
|
||||
now := TimeFunc().Unix()
|
||||
if exp, ok := token.Claims["exp"].(float64); ok {
|
||||
if now > int64(exp) {
|
||||
vErr.err = "token is expired"
|
||||
vErr.Errors |= ValidationErrorExpired
|
||||
}
|
||||
}
|
||||
if nbf, ok := token.Claims["nbf"].(float64); ok {
|
||||
if now < int64(nbf) {
|
||||
vErr.err = "token is not valid yet"
|
||||
vErr.Errors |= ValidationErrorNotValidYet
|
||||
}
|
||||
}
|
||||
|
||||
// Perform validation
|
||||
if err = token.Method.Verify(strings.Join(parts[0:2], "."), parts[2], key); err != nil {
|
||||
vErr.err = err.Error()
|
||||
vErr.Errors |= ValidationErrorSignatureInvalid
|
||||
}
|
||||
|
||||
if vErr.valid() {
|
||||
token.Valid = true
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return token, vErr
|
||||
}
|
||||
|
||||
// Try to find the token in an http.Request.
|
||||
// This method will call ParseMultipartForm if there's no token in the header.
|
||||
// Currently, it looks in the Authorization header as well as
|
||||
// looking for an 'access_token' request parameter in req.Form.
|
||||
func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) {
|
||||
|
||||
// Look for an Authorization header
|
||||
if ah := req.Header.Get("Authorization"); ah != "" {
|
||||
// Should be a bearer token
|
||||
if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" {
|
||||
return Parse(ah[7:], keyFunc)
|
||||
}
|
||||
}
|
||||
|
||||
// Look for "access_token" parameter
|
||||
req.ParseMultipartForm(10e6)
|
||||
if tokStr := req.Form.Get("access_token"); tokStr != "" {
|
||||
return Parse(tokStr, keyFunc)
|
||||
}
|
||||
|
||||
return nil, ErrNoTokenInRequest
|
||||
|
||||
}
|
||||
|
||||
// Encode JWT specific base64url encoding with padding stripped
|
||||
func EncodeSegment(seg []byte) string {
|
||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
|
||||
}
|
||||
|
||||
// Decode JWT specific base64url encoding with padding stripped
|
||||
func DecodeSegment(seg string) ([]byte, error) {
|
||||
if l := len(seg) % 4; l > 0 {
|
||||
seg += strings.Repeat("=", 4-l)
|
||||
}
|
||||
|
||||
return base64.URLEncoding.DecodeString(seg)
|
||||
}
|
187
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt_test.go
generated
vendored
Normal file
187
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt_test.go
generated
vendored
Normal file
@ -0,0 +1,187 @@
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
jwtTestDefaultKey []byte
|
||||
defaultKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return jwtTestDefaultKey, nil }
|
||||
emptyKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, nil }
|
||||
errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, fmt.Errorf("error loading key") }
|
||||
nilKeyFunc jwt.Keyfunc = nil
|
||||
)
|
||||
|
||||
var jwtTestData = []struct {
|
||||
name string
|
||||
tokenString string
|
||||
keyfunc jwt.Keyfunc
|
||||
claims map[string]interface{}
|
||||
valid bool
|
||||
errors uint32
|
||||
}{
|
||||
{
|
||||
"basic",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
defaultKeyFunc,
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
true,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"basic expired",
|
||||
"", // autogen
|
||||
defaultKeyFunc,
|
||||
map[string]interface{}{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
|
||||
false,
|
||||
jwt.ValidationErrorExpired,
|
||||
},
|
||||
{
|
||||
"basic nbf",
|
||||
"", // autogen
|
||||
defaultKeyFunc,
|
||||
map[string]interface{}{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
|
||||
false,
|
||||
jwt.ValidationErrorNotValidYet,
|
||||
},
|
||||
{
|
||||
"expired and nbf",
|
||||
"", // autogen
|
||||
defaultKeyFunc,
|
||||
map[string]interface{}{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)},
|
||||
false,
|
||||
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
|
||||
},
|
||||
{
|
||||
"basic invalid",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
defaultKeyFunc,
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorSignatureInvalid,
|
||||
},
|
||||
{
|
||||
"basic nokeyfunc",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
nilKeyFunc,
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorUnverifiable,
|
||||
},
|
||||
{
|
||||
"basic nokey",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
emptyKeyFunc,
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorSignatureInvalid,
|
||||
},
|
||||
{
|
||||
"basic errorkey",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
errorKeyFunc,
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorUnverifiable,
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
var e error
|
||||
if jwtTestDefaultKey, e = ioutil.ReadFile("test/sample_key.pub"); e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
|
||||
func makeSample(c map[string]interface{}) string {
|
||||
key, e := ioutil.ReadFile("test/sample_key")
|
||||
if e != nil {
|
||||
panic(e.Error())
|
||||
}
|
||||
|
||||
token := jwt.New(jwt.SigningMethodRS256)
|
||||
token.Claims = c
|
||||
s, e := token.SignedString(key)
|
||||
|
||||
if e != nil {
|
||||
panic(e.Error())
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func TestJWT(t *testing.T) {
|
||||
for _, data := range jwtTestData {
|
||||
if data.tokenString == "" {
|
||||
data.tokenString = makeSample(data.claims)
|
||||
}
|
||||
token, err := jwt.Parse(data.tokenString, data.keyfunc)
|
||||
|
||||
if !reflect.DeepEqual(data.claims, token.Claims) {
|
||||
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
|
||||
}
|
||||
if data.valid && err != nil {
|
||||
t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err)
|
||||
}
|
||||
if !data.valid && err == nil {
|
||||
t.Errorf("[%v] Invalid token passed validation", data.name)
|
||||
}
|
||||
if data.errors != 0 {
|
||||
if err == nil {
|
||||
t.Errorf("[%v] Expecting error. Didn't get one.", data.name)
|
||||
} else {
|
||||
// compare the bitfield part of the error
|
||||
if err.(*jwt.ValidationError).Errors != data.errors {
|
||||
t.Errorf("[%v] Errors don't match expectation", data.name)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRequest(t *testing.T) {
|
||||
// Bearer token request
|
||||
for _, data := range jwtTestData {
|
||||
if data.tokenString == "" {
|
||||
data.tokenString = makeSample(data.claims)
|
||||
}
|
||||
|
||||
r, _ := http.NewRequest("GET", "/", nil)
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString))
|
||||
token, err := jwt.ParseFromRequest(r, data.keyfunc)
|
||||
|
||||
if token == nil {
|
||||
t.Errorf("[%v] Token was not found: %v", data.name, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(data.claims, token.Claims) {
|
||||
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
|
||||
}
|
||||
if data.valid && err != nil {
|
||||
t.Errorf("[%v] Error while verifying token: %v", data.name, err)
|
||||
}
|
||||
if !data.valid && err == nil {
|
||||
t.Errorf("[%v] Invalid token passed validation", data.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method for benchmarking various methods
|
||||
func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) {
|
||||
t := jwt.New(method)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if _, err := t.SignedString(key); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
114
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa.go
generated
vendored
Normal file
114
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa.go
generated
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
)
|
||||
|
||||
// Implements the RSA family of signing methods signing methods
|
||||
type SigningMethodRSA struct {
|
||||
Name string
|
||||
Hash crypto.Hash
|
||||
}
|
||||
|
||||
// Specific instances for RS256 and company
|
||||
var (
|
||||
SigningMethodRS256 *SigningMethodRSA
|
||||
SigningMethodRS384 *SigningMethodRSA
|
||||
SigningMethodRS512 *SigningMethodRSA
|
||||
)
|
||||
|
||||
func init() {
|
||||
// RS256
|
||||
SigningMethodRS256 = &SigningMethodRSA{"RS256", crypto.SHA256}
|
||||
RegisterSigningMethod(SigningMethodRS256.Alg(), func() SigningMethod {
|
||||
return SigningMethodRS256
|
||||
})
|
||||
|
||||
// RS384
|
||||
SigningMethodRS384 = &SigningMethodRSA{"RS384", crypto.SHA384}
|
||||
RegisterSigningMethod(SigningMethodRS384.Alg(), func() SigningMethod {
|
||||
return SigningMethodRS384
|
||||
})
|
||||
|
||||
// RS512
|
||||
SigningMethodRS512 = &SigningMethodRSA{"RS512", crypto.SHA512}
|
||||
RegisterSigningMethod(SigningMethodRS512.Alg(), func() SigningMethod {
|
||||
return SigningMethodRS512
|
||||
})
|
||||
}
|
||||
|
||||
func (m *SigningMethodRSA) Alg() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
// Implements the Verify method from SigningMethod
|
||||
// For this signing method, must be either a PEM encoded PKCS1 or PKCS8 RSA public key as
|
||||
// []byte, or an rsa.PublicKey structure.
|
||||
func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error {
|
||||
var err error
|
||||
|
||||
// Decode the signature
|
||||
var sig []byte
|
||||
if sig, err = DecodeSegment(signature); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rsaKey *rsa.PublicKey
|
||||
|
||||
switch k := key.(type) {
|
||||
case []byte:
|
||||
if rsaKey, err = ParseRSAPublicKeyFromPEM(k); err != nil {
|
||||
return err
|
||||
}
|
||||
case *rsa.PublicKey:
|
||||
rsaKey = k
|
||||
default:
|
||||
return ErrInvalidKey
|
||||
}
|
||||
|
||||
// Create hasher
|
||||
if !m.Hash.Available() {
|
||||
return ErrHashUnavailable
|
||||
}
|
||||
hasher := m.Hash.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
|
||||
// Verify the signature
|
||||
return rsa.VerifyPKCS1v15(rsaKey, m.Hash, hasher.Sum(nil), sig)
|
||||
}
|
||||
|
||||
// Implements the Sign method from SigningMethod
|
||||
// For this signing method, must be either a PEM encoded PKCS1 or PKCS8 RSA private key as
|
||||
// []byte, or an rsa.PrivateKey structure.
|
||||
func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) {
|
||||
var err error
|
||||
var rsaKey *rsa.PrivateKey
|
||||
|
||||
switch k := key.(type) {
|
||||
case []byte:
|
||||
if rsaKey, err = ParseRSAPrivateKeyFromPEM(k); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
rsaKey = k
|
||||
default:
|
||||
return "", ErrInvalidKey
|
||||
}
|
||||
|
||||
// Create the hasher
|
||||
if !m.Hash.Available() {
|
||||
return "", ErrHashUnavailable
|
||||
}
|
||||
|
||||
hasher := m.Hash.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
|
||||
// Sign the string and return the encoded bytes
|
||||
if sigBytes, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil)); err == nil {
|
||||
return EncodeSegment(sigBytes), nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
174
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_test.go
generated
vendored
Normal file
174
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_test.go
generated
vendored
Normal file
@ -0,0 +1,174 @@
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var rsaTestData = []struct {
|
||||
name string
|
||||
tokenString string
|
||||
alg string
|
||||
claims map[string]interface{}
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
"Basic RS256",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
"RS256",
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Basic RS384",
|
||||
"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.W-jEzRfBigtCWsinvVVuldiuilzVdU5ty0MvpLaSaqK9PlAWWlDQ1VIQ_qSKzwL5IXaZkvZFJXT3yL3n7OUVu7zCNJzdwznbC8Z-b0z2lYvcklJYi2VOFRcGbJtXUqgjk2oGsiqUMUMOLP70TTefkpsgqDxbRh9CDUfpOJgW-dU7cmgaoswe3wjUAUi6B6G2YEaiuXC0XScQYSYVKIzgKXJV8Zw-7AN_DBUI4GkTpsvQ9fVVjZM9csQiEXhYekyrKu1nu_POpQonGd8yqkIyXPECNmmqH5jH4sFiF67XhD7_JpkvLziBpI-uh86evBUadmHhb9Otqw3uV3NTaXLzJw",
|
||||
"RS384",
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Basic RS512",
|
||||
"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.zBlLlmRrUxx4SJPUbV37Q1joRcI9EW13grnKduK3wtYKmDXbgDpF1cZ6B-2Jsm5RB8REmMiLpGms-EjXhgnyh2TSHE-9W2gA_jvshegLWtwRVDX40ODSkTb7OVuaWgiy9y7llvcknFBTIg-FnVPVpXMmeV_pvwQyhaz1SSwSPrDyxEmksz1hq7YONXhXPpGaNbMMeDTNP_1oj8DZaqTIL9TwV8_1wb2Odt_Fy58Ke2RVFijsOLdnyEAjt2n9Mxihu9i3PhNBkkxa2GbnXBfq3kzvZ_xxGGopLdHhJjcGWXO-NiwI9_tiu14NRv4L2xC0ItD9Yz68v2ZIZEp_DuzwRQ",
|
||||
"RS512",
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"basic invalid: foo => bar",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
"RS256",
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestRSAVerify(t *testing.T) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key.pub")
|
||||
|
||||
for _, data := range rsaTestData {
|
||||
parts := strings.Split(data.tokenString, ".")
|
||||
|
||||
method := jwt.GetSigningMethod(data.alg)
|
||||
err := method.Verify(strings.Join(parts[0:2], "."), parts[2], key)
|
||||
if data.valid && err != nil {
|
||||
t.Errorf("[%v] Error while verifying key: %v", data.name, err)
|
||||
}
|
||||
if !data.valid && err == nil {
|
||||
t.Errorf("[%v] Invalid key passed validation", data.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRSASign(t *testing.T) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
|
||||
for _, data := range rsaTestData {
|
||||
if data.valid {
|
||||
parts := strings.Split(data.tokenString, ".")
|
||||
method := jwt.GetSigningMethod(data.alg)
|
||||
sig, err := method.Sign(strings.Join(parts[0:2], "."), key)
|
||||
if err != nil {
|
||||
t.Errorf("[%v] Error signing token: %v", data.name, err)
|
||||
}
|
||||
if sig != parts[2] {
|
||||
t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", data.name, sig, parts[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRSAVerifyWithPreParsedPrivateKey(t *testing.T) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key.pub")
|
||||
parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testData := rsaTestData[0]
|
||||
parts := strings.Split(testData.tokenString, ".")
|
||||
err = jwt.SigningMethodRS256.Verify(strings.Join(parts[0:2], "."), parts[2], parsedKey)
|
||||
if err != nil {
|
||||
t.Errorf("[%v] Error while verifying key: %v", testData.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRSAWithPreParsedPrivateKey(t *testing.T) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testData := rsaTestData[0]
|
||||
parts := strings.Split(testData.tokenString, ".")
|
||||
sig, err := jwt.SigningMethodRS256.Sign(strings.Join(parts[0:2], "."), parsedKey)
|
||||
if err != nil {
|
||||
t.Errorf("[%v] Error signing token: %v", testData.name, err)
|
||||
}
|
||||
if sig != parts[2] {
|
||||
t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", testData.name, sig, parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRSAKeyParsing(t *testing.T) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
pubKey, _ := ioutil.ReadFile("test/sample_key.pub")
|
||||
badKey := []byte("All your base are belong to key")
|
||||
|
||||
// Test parsePrivateKey
|
||||
if _, e := jwt.ParseRSAPrivateKeyFromPEM(key); e != nil {
|
||||
t.Errorf("Failed to parse valid private key: %v", e)
|
||||
}
|
||||
|
||||
if k, e := jwt.ParseRSAPrivateKeyFromPEM(pubKey); e == nil {
|
||||
t.Errorf("Parsed public key as valid private key: %v", k)
|
||||
}
|
||||
|
||||
if k, e := jwt.ParseRSAPrivateKeyFromPEM(badKey); e == nil {
|
||||
t.Errorf("Parsed invalid key as valid private key: %v", k)
|
||||
}
|
||||
|
||||
// Test parsePublicKey
|
||||
if _, e := jwt.ParseRSAPublicKeyFromPEM(pubKey); e != nil {
|
||||
t.Errorf("Failed to parse valid public key: %v", e)
|
||||
}
|
||||
|
||||
if k, e := jwt.ParseRSAPublicKeyFromPEM(key); e == nil {
|
||||
t.Errorf("Parsed private key as valid public key: %v", k)
|
||||
}
|
||||
|
||||
if k, e := jwt.ParseRSAPublicKeyFromPEM(badKey); e == nil {
|
||||
t.Errorf("Parsed invalid key as valid private key: %v", k)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkRS256Signing(b *testing.B) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
benchmarkSigning(b, jwt.SigningMethodRS256, parsedKey)
|
||||
}
|
||||
|
||||
func BenchmarkRS384Signing(b *testing.B) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
benchmarkSigning(b, jwt.SigningMethodRS384, parsedKey)
|
||||
}
|
||||
|
||||
func BenchmarkRS512Signing(b *testing.B) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
benchmarkSigning(b, jwt.SigningMethodRS512, parsedKey)
|
||||
}
|
68
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_utils.go
generated
vendored
Normal file
68
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_utils.go
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
|
||||
ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key")
|
||||
)
|
||||
|
||||
// Parse PEM encoded PKCS1 or PKCS8 private key
|
||||
func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
|
||||
var err error
|
||||
|
||||
// Parse PEM block
|
||||
var block *pem.Block
|
||||
if block, _ = pem.Decode(key); block == nil {
|
||||
return nil, ErrKeyMustBePEMEncoded
|
||||
}
|
||||
|
||||
var parsedKey interface{}
|
||||
if parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
|
||||
if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var pkey *rsa.PrivateKey
|
||||
var ok bool
|
||||
if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok {
|
||||
return nil, ErrNotRSAPrivateKey
|
||||
}
|
||||
|
||||
return pkey, nil
|
||||
}
|
||||
|
||||
// Parse PEM encoded PKCS1 or PKCS8 public key
|
||||
func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
|
||||
var err error
|
||||
|
||||
// Parse PEM block
|
||||
var block *pem.Block
|
||||
if block, _ = pem.Decode(key); block == nil {
|
||||
return nil, ErrKeyMustBePEMEncoded
|
||||
}
|
||||
|
||||
// Parse the key
|
||||
var parsedKey interface{}
|
||||
if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil {
|
||||
if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
|
||||
parsedKey = cert.PublicKey
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var pkey *rsa.PublicKey
|
||||
var ok bool
|
||||
if pkey, ok = parsedKey.(*rsa.PublicKey); !ok {
|
||||
return nil, ErrNotRSAPrivateKey
|
||||
}
|
||||
|
||||
return pkey, nil
|
||||
}
|
24
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/signing_method.go
generated
vendored
Normal file
24
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/signing_method.go
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
package jwt
|
||||
|
||||
var signingMethods = map[string]func() SigningMethod{}
|
||||
|
||||
// Signing method
|
||||
type SigningMethod interface {
|
||||
Verify(signingString, signature string, key interface{}) error
|
||||
Sign(signingString string, key interface{}) (string, error)
|
||||
Alg() string
|
||||
}
|
||||
|
||||
// Register the "alg" name and a factory function for signing method.
|
||||
// This is typically done during init() in the method's implementation
|
||||
func RegisterSigningMethod(alg string, f func() SigningMethod) {
|
||||
signingMethods[alg] = f
|
||||
}
|
||||
|
||||
// Get a signing method from an "alg" string
|
||||
func GetSigningMethod(alg string) (method SigningMethod) {
|
||||
if methodF, ok := signingMethods[alg]; ok {
|
||||
method = methodF()
|
||||
}
|
||||
return
|
||||
}
|
1
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/hmacTestKey
generated
vendored
Normal file
1
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/hmacTestKey
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
#5K+・シミew{ヲ住ウ(跼Tノ(ゥ┫メP.ソモ燾辻G<>感テwb="=.!r.Oタヘ奎gミ」
|
27
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/sample_key
generated
vendored
Normal file
27
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/sample_key
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEU/wT8RDtn
|
||||
SgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0i
|
||||
cqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhC
|
||||
PUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR+1DcKJzQBSTAGnpYVaqpsAR
|
||||
ap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKA
|
||||
Rdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7wIDAQABAoIBAQCwia1k7+2oZ2d3
|
||||
n6agCAbqIE1QXfCmh41ZqJHbOY3oRQG3X1wpcGH4Gk+O+zDVTV2JszdcOt7E5dAy
|
||||
MaomETAhRxB7hlIOnEN7WKm+dGNrKRvV0wDU5ReFMRHg31/Lnu8c+5BvGjZX+ky9
|
||||
POIhFFYJqwCRlopGSUIxmVj5rSgtzk3iWOQXr+ah1bjEXvlxDOWkHN6YfpV5ThdE
|
||||
KdBIPGEVqa63r9n2h+qazKrtiRqJqGnOrHzOECYbRFYhexsNFz7YT02xdfSHn7gM
|
||||
IvabDDP/Qp0PjE1jdouiMaFHYnLBbgvlnZW9yuVf/rpXTUq/njxIXMmvmEyyvSDn
|
||||
FcFikB8pAoGBAPF77hK4m3/rdGT7X8a/gwvZ2R121aBcdPwEaUhvj/36dx596zvY
|
||||
mEOjrWfZhF083/nYWE2kVquj2wjs+otCLfifEEgXcVPTnEOPO9Zg3uNSL0nNQghj
|
||||
FuD3iGLTUBCtM66oTe0jLSslHe8gLGEQqyMzHOzYxNqibxcOZIe8Qt0NAoGBAO+U
|
||||
I5+XWjWEgDmvyC3TrOSf/KCGjtu0TSv30ipv27bDLMrpvPmD/5lpptTFwcxvVhCs
|
||||
2b+chCjlghFSWFbBULBrfci2FtliClOVMYrlNBdUSJhf3aYSG2Doe6Bgt1n2CpNn
|
||||
/iu37Y3NfemZBJA7hNl4dYe+f+uzM87cdQ214+jrAoGAXA0XxX8ll2+ToOLJsaNT
|
||||
OvNB9h9Uc5qK5X5w+7G7O998BN2PC/MWp8H+2fVqpXgNENpNXttkRm1hk1dych86
|
||||
EunfdPuqsX+as44oCyJGFHVBnWpm33eWQw9YqANRI+pCJzP08I5WK3osnPiwshd+
|
||||
hR54yjgfYhBFNI7B95PmEQkCgYBzFSz7h1+s34Ycr8SvxsOBWxymG5zaCsUbPsL0
|
||||
4aCgLScCHb9J+E86aVbbVFdglYa5Id7DPTL61ixhl7WZjujspeXZGSbmq0Kcnckb
|
||||
mDgqkLECiOJW2NHP/j0McAkDLL4tysF8TLDO8gvuvzNC+WQ6drO2ThrypLVZQ+ry
|
||||
eBIPmwKBgEZxhqa0gVvHQG/7Od69KWj4eJP28kq13RhKay8JOoN0vPmspXJo1HY3
|
||||
CKuHRG+AP579dncdUnOMvfXOtkdM4vk0+hWASBQzM9xzVcztCa+koAugjVaLS9A+
|
||||
9uQoqEeVNTckxx0S2bYevRy7hGQmUJTyQm3j1zEUR5jpdbL83Fbq
|
||||
-----END RSA PRIVATE KEY-----
|
9
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/sample_key.pub
generated
vendored
Normal file
9
Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/sample_key.pub
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4f5wg5l2hKsTeNem/V41
|
||||
fGnJm6gOdrj8ym3rFkEU/wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7
|
||||
mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBp
|
||||
HssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2
|
||||
XrHhR+1DcKJzQBSTAGnpYVaqpsARap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3b
|
||||
ODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy
|
||||
7wIDAQAB
|
||||
-----END PUBLIC KEY-----
|
@ -72,7 +72,7 @@ DNS_DOMAIN="kubernetes.local"
|
||||
DNS_REPLICAS=1
|
||||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
|
||||
# Optional: Enable/disable public IP assignment for minions.
|
||||
# Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes!
|
||||
|
@ -49,4 +49,4 @@ ELASTICSEARCH_LOGGING_REPLICAS=1
|
||||
ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-true}"
|
||||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
|
@ -76,4 +76,4 @@ DNS_DOMAIN="kubernetes.local"
|
||||
DNS_REPLICAS=1
|
||||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
|
@ -74,4 +74,4 @@ DNS_SERVER_IP="10.0.0.10"
|
||||
DNS_DOMAIN="kubernetes.local"
|
||||
DNS_REPLICAS=1
|
||||
|
||||
ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
|
@ -24,6 +24,8 @@
|
||||
{% if grains.cloud is defined -%}
|
||||
{% set cloud_provider = "--cloud_provider=" + grains.cloud -%}
|
||||
|
||||
{% set service_account_key = " --service_account_private_key_file=/srv/kubernetes/server.key " -%}
|
||||
|
||||
{% if grains.cloud == 'gce' -%}
|
||||
{% if grains.cloud_config is defined -%}
|
||||
{% set cloud_config = "--cloud_config=" + grains.cloud_config -%}
|
||||
@ -55,7 +57,7 @@
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
|
||||
{% set params = "--master=127.0.0.1:8080" + " " + machines + " " + cluster_name + " " + cluster_cidr + " " + allocate_node_cidrs + " " + minion_regexp + " " + cloud_provider + " " + sync_nodes + " " + cloud_config + " " + pillar['log_level'] -%}
|
||||
{% set params = "--master=127.0.0.1:8080" + " " + machines + " " + cluster_name + " " + cluster_cidr + " " + allocate_node_cidrs + " " + minion_regexp + " " + cloud_provider + " " + sync_nodes + " " + cloud_config + service_account_key + pillar['log_level'] -%}
|
||||
|
||||
{
|
||||
"apiVersion": "v1beta3",
|
||||
|
@ -50,7 +50,7 @@ MASTER_USER=vagrant
|
||||
MASTER_PASSWD=vagrant
|
||||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
|
||||
# Optional: Install node monitoring.
|
||||
ENABLE_NODE_MONITORING=true
|
||||
|
@ -37,4 +37,5 @@ import (
|
||||
_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle"
|
||||
_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota"
|
||||
_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
|
||||
_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount"
|
||||
)
|
||||
|
@ -67,6 +67,8 @@ type APIServer struct {
|
||||
BasicAuthFile string
|
||||
ClientCAFile string
|
||||
TokenAuthFile string
|
||||
ServiceAccountKeyFile string
|
||||
ServiceAccountLookup bool
|
||||
AuthorizationMode string
|
||||
AuthorizationPolicyFile string
|
||||
AdmissionControl string
|
||||
@ -162,6 +164,8 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.StringVar(&s.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, "If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.")
|
||||
fs.StringVar(&s.ClientCAFile, "client-ca-file", s.ClientCAFile, "If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.")
|
||||
fs.StringVar(&s.TokenAuthFile, "token-auth-file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.")
|
||||
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.")
|
||||
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
|
||||
fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
|
||||
fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
|
||||
fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", "))
|
||||
@ -272,7 +276,11 @@ func (s *APIServer) Run(_ []string) error {
|
||||
|
||||
n := net.IPNet(s.PortalNet)
|
||||
|
||||
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile)
|
||||
// Default to the private server key for service account token signing
|
||||
if s.ServiceAccountKeyFile == "" && s.TLSPrivateKeyFile != "" {
|
||||
s.ServiceAccountKeyFile = s.TLSPrivateKeyFile
|
||||
}
|
||||
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, client)
|
||||
if err != nil {
|
||||
glog.Fatalf("Invalid Authentication Config: %v", err)
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/namespace"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/service"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder"
|
||||
|
||||
@ -73,6 +74,7 @@ type CMServer struct {
|
||||
PodEvictionTimeout time.Duration
|
||||
DeletingPodsQps float32
|
||||
DeletingPodsBurst int
|
||||
ServiceAccountKeyFile string
|
||||
|
||||
// TODO: Discover these by pinging the host machines, and rip out these params.
|
||||
NodeMilliCPU int64
|
||||
@ -141,6 +143,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) {
|
||||
"Amount of time which we allow starting Node to be unresponsive before marking it unhealty.")
|
||||
fs.DurationVar(&s.NodeMonitorPeriod, "node-monitor-period", 5*time.Second,
|
||||
"The period for syncing NodeStatus in NodeController.")
|
||||
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-private-key-file", s.ServiceAccountKeyFile, "Filename containing a PEM-encoded private RSA key used to sign service account tokens.")
|
||||
// TODO: Discover these by pinging the host machines, and rip out these flags.
|
||||
// TODO: in the meantime, use resource.QuantityFlag() instead of these
|
||||
fs.Int64Var(&s.NodeMilliCPU, "node-milli-cpu", s.NodeMilliCPU, "The amount of MilliCPU provisioned on each node")
|
||||
@ -249,6 +252,25 @@ func (s *CMServer) Run(_ []string) error {
|
||||
pvclaimBinder.Run()
|
||||
}
|
||||
|
||||
if len(s.ServiceAccountKeyFile) > 0 {
|
||||
privateKey, err := serviceaccount.ReadPrivateKey(s.ServiceAccountKeyFile)
|
||||
if err != nil {
|
||||
glog.Errorf("Error reading key for service account token controller: %v", err)
|
||||
} else {
|
||||
serviceaccount.NewTokensController(
|
||||
kubeClient,
|
||||
serviceaccount.DefaultTokenControllerOptions(
|
||||
serviceaccount.JWTTokenGenerator(privateKey),
|
||||
),
|
||||
).Run()
|
||||
}
|
||||
}
|
||||
|
||||
serviceaccount.NewServiceAccountsController(
|
||||
kubeClient,
|
||||
serviceaccount.DefaultServiceAccountControllerOptions(),
|
||||
).Run()
|
||||
|
||||
select {}
|
||||
return nil
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ KUBE_SERVICE_ADDRESSES="--portal_net={{ kube_service_addresses }}"
|
||||
KUBE_ETCD_SERVERS="--etcd_servers=http://{{ groups['etcd'][0] }}:2379"
|
||||
|
||||
# default admission control policies
|
||||
KUBE_ADMISSION_CONTROL="--admission_control=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota"
|
||||
KUBE_ADMISSION_CONTROL="--admission_control=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota"
|
||||
|
||||
# Add your own!
|
||||
KUBE_API_ARGS=""
|
||||
|
@ -259,6 +259,7 @@ _kubectl_get()
|
||||
must_have_one_noun+=("resourcequota")
|
||||
must_have_one_noun+=("secret")
|
||||
must_have_one_noun+=("service")
|
||||
must_have_one_noun+=("serviceaccount")
|
||||
}
|
||||
|
||||
_kubectl_describe()
|
||||
@ -284,7 +285,9 @@ _kubectl_describe()
|
||||
must_have_one_noun+=("pod")
|
||||
must_have_one_noun+=("replicationcontroller")
|
||||
must_have_one_noun+=("resourcequota")
|
||||
must_have_one_noun+=("secret")
|
||||
must_have_one_noun+=("service")
|
||||
must_have_one_noun+=("serviceaccount")
|
||||
}
|
||||
|
||||
_kubectl_create()
|
||||
|
@ -132,12 +132,21 @@ trap cleanup EXIT
|
||||
echo "Starting etcd"
|
||||
kube::etcd::start
|
||||
|
||||
SERVICE_ACCOUNT_LOOKUP=${SERVICE_ACCOUNT_LOOKUP:-false}
|
||||
SERVICE_ACCOUNT_KEY=${SERVICE_ACCOUNT_KEY:-"/var/run/kubernetes/serviceaccount.key"}
|
||||
# Generate ServiceAccount key if needed
|
||||
if [[ ! -f "${SERVICE_ACCOUNT_KEY}" ]]; then
|
||||
openssl genrsa -out "${SERVICE_ACCOUNT_KEY}" 2048 2>/dev/null
|
||||
fi
|
||||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
|
||||
APISERVER_LOG=/tmp/kube-apiserver.log
|
||||
sudo -E "${GO_OUT}/kube-apiserver" \
|
||||
--v=${LOG_LEVEL} \
|
||||
--service_account_key_file="${SERVICE_ACCOUNT_KEY}" \
|
||||
--service_account_lookup="${SERVICE_ACCOUNT_LOOKUP}" \
|
||||
--admission_control="${ADMISSION_CONTROL}" \
|
||||
--address="${API_HOST}" \
|
||||
--port="${API_PORT}" \
|
||||
@ -155,6 +164,7 @@ CTLRMGR_LOG=/tmp/kube-controller-manager.log
|
||||
sudo -E "${GO_OUT}/kube-controller-manager" \
|
||||
--v=${LOG_LEVEL} \
|
||||
--machines="127.0.0.1" \
|
||||
--service_account_private_key_file="${SERVICE_ACCOUNT_KEY}" \
|
||||
--master="${API_HOST}:${API_PORT}" >"${CTLRMGR_LOG}" 2>&1 &
|
||||
CTLRMGR_PID=$!
|
||||
|
||||
|
@ -51,6 +51,8 @@ func init() {
|
||||
&ResourceQuotaList{},
|
||||
&Namespace{},
|
||||
&NamespaceList{},
|
||||
&ServiceAccount{},
|
||||
&ServiceAccountList{},
|
||||
&Secret{},
|
||||
&SecretList{},
|
||||
&PersistentVolume{},
|
||||
@ -98,6 +100,8 @@ func (*ResourceQuota) IsAnAPIObject() {}
|
||||
func (*ResourceQuotaList) IsAnAPIObject() {}
|
||||
func (*Namespace) IsAnAPIObject() {}
|
||||
func (*NamespaceList) IsAnAPIObject() {}
|
||||
func (*ServiceAccount) IsAnAPIObject() {}
|
||||
func (*ServiceAccountList) IsAnAPIObject() {}
|
||||
func (*Secret) IsAnAPIObject() {}
|
||||
func (*SecretList) IsAnAPIObject() {}
|
||||
func (*PersistentVolume) IsAnAPIObject() {}
|
||||
|
@ -814,6 +814,10 @@ type PodSpec struct {
|
||||
// NodeSelector is a selector which must be true for the pod to fit on a node
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
|
||||
|
||||
// ServiceAccount is the name of the ServiceAccount to use to run this pod
|
||||
// The pod will be allowed to use secrets referenced by the ServiceAccount
|
||||
ServiceAccount string `json:"serviceAccount"`
|
||||
|
||||
// Host is a request to schedule this pod onto a specific host. If it is non-empty,
|
||||
// the the scheduler simply schedules this pod onto that host, assuming that it fits
|
||||
// resource requirements.
|
||||
@ -1035,6 +1039,26 @@ type Service struct {
|
||||
Status ServiceStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceAccount binds together:
|
||||
// * a name, understood by users, and perhaps by peripheral systems, for an identity
|
||||
// * a principal that can be authenticated and authorized
|
||||
// * a set of secrets
|
||||
type ServiceAccount struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
|
||||
Secrets []ObjectReference `json:"secrets"`
|
||||
}
|
||||
|
||||
// ServiceAccountList is a list of ServiceAccount objects
|
||||
type ServiceAccountList struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []ServiceAccount `json:"items"`
|
||||
}
|
||||
|
||||
// Endpoints is a collection of endpoints that implement the actual service. Example:
|
||||
// Name: "mysvc",
|
||||
// Subsets: [
|
||||
@ -1805,7 +1829,25 @@ const MaxSecretSize = 1 * 1024 * 1024
|
||||
type SecretType string
|
||||
|
||||
const (
|
||||
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data
|
||||
// SecretTypeOpaque is the default; arbitrary user-defined data
|
||||
SecretTypeOpaque SecretType = "Opaque"
|
||||
|
||||
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
|
||||
//
|
||||
// Required fields:
|
||||
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
|
||||
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
|
||||
// - Secret.Data["token"] - a token that identifies the service account to the API
|
||||
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
||||
|
||||
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
||||
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
|
||||
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountTokenKey = "token"
|
||||
// ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountKubeconfigKey = "kubernetes.kubeconfig"
|
||||
)
|
||||
|
||||
type SecretList struct {
|
||||
|
@ -1880,6 +1880,7 @@ func init() {
|
||||
out.NodeSelector[key] = val
|
||||
}
|
||||
}
|
||||
out.ServiceAccount = in.ServiceAccount
|
||||
out.Host = in.Host
|
||||
out.HostNetwork = in.HostNetwork
|
||||
return nil
|
||||
@ -1913,6 +1914,7 @@ func init() {
|
||||
out.NodeSelector[key] = val
|
||||
}
|
||||
}
|
||||
out.ServiceAccount = in.ServiceAccount
|
||||
out.Host = in.Host
|
||||
out.HostNetwork = in.HostNetwork
|
||||
return nil
|
||||
@ -2477,6 +2479,74 @@ func init() {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(in *ServiceAccount, out *newer.ServiceAccount, s conversion.Scope) error {
|
||||
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Secrets != nil {
|
||||
out.Secrets = make([]newer.ObjectReference, len(in.Secrets))
|
||||
for i := range in.Secrets {
|
||||
if err := s.Convert(&in.Secrets[i], &out.Secrets[i], 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(in *newer.ServiceAccount, out *ServiceAccount, s conversion.Scope) error {
|
||||
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Secrets != nil {
|
||||
out.Secrets = make([]ObjectReference, len(in.Secrets))
|
||||
for i := range in.Secrets {
|
||||
if err := s.Convert(&in.Secrets[i], &out.Secrets[i], 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(in *ServiceAccountList, out *newer.ServiceAccountList, s conversion.Scope) error {
|
||||
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Items != nil {
|
||||
out.Items = make([]newer.ServiceAccount, len(in.Items))
|
||||
for i := range in.Items {
|
||||
if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(in *newer.ServiceAccountList, out *ServiceAccountList, s conversion.Scope) error {
|
||||
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Items != nil {
|
||||
out.Items = make([]ServiceAccount, len(in.Items))
|
||||
for i := range in.Items {
|
||||
if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(in *ServiceList, out *newer.ServiceList, s conversion.Scope) error {
|
||||
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
|
||||
return err
|
||||
|
@ -52,6 +52,8 @@ func init() {
|
||||
&NamespaceList{},
|
||||
&Secret{},
|
||||
&SecretList{},
|
||||
&ServiceAccount{},
|
||||
&ServiceAccountList{},
|
||||
&PersistentVolume{},
|
||||
&PersistentVolumeList{},
|
||||
&PersistentVolumeClaim{},
|
||||
@ -97,6 +99,8 @@ func (*Namespace) IsAnAPIObject() {}
|
||||
func (*NamespaceList) IsAnAPIObject() {}
|
||||
func (*Secret) IsAnAPIObject() {}
|
||||
func (*SecretList) IsAnAPIObject() {}
|
||||
func (*ServiceAccount) IsAnAPIObject() {}
|
||||
func (*ServiceAccountList) IsAnAPIObject() {}
|
||||
func (*PersistentVolume) IsAnAPIObject() {}
|
||||
func (*PersistentVolumeList) IsAnAPIObject() {}
|
||||
func (*PersistentVolumeClaim) IsAnAPIObject() {}
|
||||
|
@ -816,6 +816,9 @@ type PodSpec struct {
|
||||
// NodeSelector is a selector which must be true for the pod to fit on a node
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"`
|
||||
|
||||
// ServiceAccount is the name of the ServiceAccount to use to run this pod
|
||||
ServiceAccount string `json:"serviceAccount" description:"name of the ServiceAccount to use to run this pod"`
|
||||
|
||||
// Host is a request to schedule this pod onto a specific host. If it is non-empty,
|
||||
// the the scheduler simply schedules this pod onto that host, assuming that it fits
|
||||
// resource requirements.
|
||||
@ -1036,6 +1039,26 @@ type ServiceList struct {
|
||||
Items []Service `json:"items" description:"list of services"`
|
||||
}
|
||||
|
||||
// ServiceAccount binds together:
|
||||
// * a name, understood by users, and perhaps by peripheral systems, for an identity
|
||||
// * a principal that can be authenticated and authorized
|
||||
// * a set of secrets
|
||||
type ServiceAccount struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://docs.k8s.io/api-conventions.md#metadata"`
|
||||
|
||||
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
|
||||
Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
}
|
||||
|
||||
// ServiceAccountList is a list of ServiceAccount objects
|
||||
type ServiceAccountList struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"`
|
||||
|
||||
Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"`
|
||||
}
|
||||
|
||||
// Endpoints is a collection of endpoints that implement the actual service. Example:
|
||||
// Name: "mysvc",
|
||||
// Subsets: [
|
||||
@ -1708,7 +1731,23 @@ const MaxSecretSize = 1 * 1024 * 1024
|
||||
type SecretType string
|
||||
|
||||
const (
|
||||
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data
|
||||
// SecretTypeOpaque is the default; arbitrary user-defined data
|
||||
SecretTypeOpaque SecretType = "Opaque"
|
||||
|
||||
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
|
||||
//
|
||||
// Required fields:
|
||||
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
|
||||
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
|
||||
// - Secret.Data["token"] - a token that identifies the service account to the API
|
||||
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
||||
|
||||
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
||||
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
|
||||
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountTokenKey = "token"
|
||||
)
|
||||
|
||||
type SecretList struct {
|
||||
|
@ -377,6 +377,7 @@ func init() {
|
||||
}
|
||||
out.DesiredState.Host = in.Spec.Host
|
||||
out.CurrentState.Host = in.Spec.Host
|
||||
out.ServiceAccount = in.Spec.ServiceAccount
|
||||
if err := s.Convert(&in.Status, &out.CurrentState, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -399,6 +400,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
out.Spec.Host = in.DesiredState.Host
|
||||
out.Spec.ServiceAccount = in.ServiceAccount
|
||||
if err := s.Convert(&in.CurrentState, &out.Status, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -503,6 +505,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
out.DesiredState.Host = in.Spec.Host
|
||||
out.ServiceAccount = in.Spec.ServiceAccount
|
||||
if err := s.Convert(&in.Spec.NodeSelector, &out.NodeSelector, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -519,6 +522,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
out.Spec.Host = in.DesiredState.Host
|
||||
out.Spec.ServiceAccount = in.ServiceAccount
|
||||
if err := s.Convert(&in.NodeSelector, &out.Spec.NodeSelector, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1685,4 +1689,17 @@ func init() {
|
||||
// If one of the conversion functions is malformed, detect it immediately.
|
||||
panic(err)
|
||||
}
|
||||
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "ServiceAccount",
|
||||
func(label, value string) (string, string, error) {
|
||||
switch label {
|
||||
case "name":
|
||||
return "metadata.name", value, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("field label not supported: %s", label)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
// If one of the conversion functions is malformed, detect it immediately.
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,8 @@ func init() {
|
||||
&NamespaceList{},
|
||||
&Secret{},
|
||||
&SecretList{},
|
||||
&ServiceAccount{},
|
||||
&ServiceAccountList{},
|
||||
&PersistentVolume{},
|
||||
&PersistentVolumeList{},
|
||||
&PersistentVolumeClaim{},
|
||||
@ -105,6 +107,8 @@ func (*Namespace) IsAnAPIObject() {}
|
||||
func (*NamespaceList) IsAnAPIObject() {}
|
||||
func (*Secret) IsAnAPIObject() {}
|
||||
func (*SecretList) IsAnAPIObject() {}
|
||||
func (*ServiceAccount) IsAnAPIObject() {}
|
||||
func (*ServiceAccountList) IsAnAPIObject() {}
|
||||
func (*PersistentVolume) IsAnAPIObject() {}
|
||||
func (*PersistentVolumeList) IsAnAPIObject() {}
|
||||
func (*PersistentVolumeClaim) IsAnAPIObject() {}
|
||||
|
@ -755,6 +755,8 @@ type Pod struct {
|
||||
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"`
|
||||
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"`
|
||||
CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"`
|
||||
// ServiceAccount is the name of the ServiceAccount to use to run this pod
|
||||
ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"`
|
||||
// NodeSelector is a selector which must be true for the pod to fit on a node
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"`
|
||||
}
|
||||
@ -782,10 +784,11 @@ type ReplicationController struct {
|
||||
|
||||
// PodTemplate holds the information used for creating pods.
|
||||
type PodTemplate struct {
|
||||
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"`
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"`
|
||||
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"`
|
||||
Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"`
|
||||
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"`
|
||||
ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"`
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"`
|
||||
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"`
|
||||
Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"`
|
||||
}
|
||||
|
||||
// Session Affinity Type string
|
||||
@ -884,6 +887,24 @@ type ServicePort struct {
|
||||
ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"`
|
||||
}
|
||||
|
||||
// ServiceAccount binds together:
|
||||
// * a name, understood by users, and perhaps by peripheral systems, for an identity
|
||||
// * a principal that can be authenticated and authorized
|
||||
// * a set of secrets
|
||||
type ServiceAccount struct {
|
||||
TypeMeta `json:",inline"`
|
||||
|
||||
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
|
||||
Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
}
|
||||
|
||||
// ServiceAccountList is a list of ServiceAccount objects
|
||||
type ServiceAccountList struct {
|
||||
TypeMeta `json:",inline"`
|
||||
|
||||
Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"`
|
||||
}
|
||||
|
||||
// EndpointObjectReference is a reference to an object exposing the endpoint
|
||||
type EndpointObjectReference struct {
|
||||
Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"`
|
||||
@ -1617,7 +1638,23 @@ const MaxSecretSize = 1 * 1024 * 1024
|
||||
type SecretType string
|
||||
|
||||
const (
|
||||
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data
|
||||
// SecretTypeOpaque is the default; arbitrary user-defined data
|
||||
SecretTypeOpaque SecretType = "Opaque"
|
||||
|
||||
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
|
||||
//
|
||||
// Required fields:
|
||||
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
|
||||
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
|
||||
// - Secret.Data["token"] - a token that identifies the service account to the API
|
||||
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
||||
|
||||
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
||||
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
|
||||
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountTokenKey = "token"
|
||||
)
|
||||
|
||||
type SecretList struct {
|
||||
|
@ -180,6 +180,7 @@ func init() {
|
||||
}
|
||||
out.DesiredState.Host = in.Spec.Host
|
||||
out.CurrentState.Host = in.Spec.Host
|
||||
out.ServiceAccount = in.Spec.ServiceAccount
|
||||
if err := s.Convert(&in.Status, &out.CurrentState, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -201,6 +202,7 @@ func init() {
|
||||
if err := s.Convert(&in.DesiredState.Manifest, &out.Spec, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
out.Spec.ServiceAccount = in.ServiceAccount
|
||||
out.Spec.Host = in.DesiredState.Host
|
||||
if err := s.Convert(&in.CurrentState, &out.Status, 0); err != nil {
|
||||
return err
|
||||
@ -282,6 +284,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
out.DesiredState.Host = in.Spec.Host
|
||||
out.ServiceAccount = in.Spec.ServiceAccount
|
||||
if err := s.Convert(&in.Spec.NodeSelector, &out.NodeSelector, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -298,6 +301,7 @@ func init() {
|
||||
return err
|
||||
}
|
||||
out.Spec.Host = in.DesiredState.Host
|
||||
out.Spec.ServiceAccount = in.ServiceAccount
|
||||
if err := s.Convert(&in.NodeSelector, &out.Spec.NodeSelector, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1601,4 +1605,17 @@ func init() {
|
||||
// If one of the conversion functions is malformed, detect it immediately.
|
||||
panic(err)
|
||||
}
|
||||
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "ServiceAccount",
|
||||
func(label, value string) (string, string, error) {
|
||||
switch label {
|
||||
case "name":
|
||||
return "metadata.name", value, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("field label not supported: %s", label)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
// If one of the conversion functions is malformed, detect it immediately.
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,8 @@ func init() {
|
||||
&NamespaceList{},
|
||||
&Secret{},
|
||||
&SecretList{},
|
||||
&ServiceAccount{},
|
||||
&ServiceAccountList{},
|
||||
&PersistentVolume{},
|
||||
&PersistentVolumeList{},
|
||||
&PersistentVolumeClaim{},
|
||||
@ -105,6 +107,8 @@ func (*Namespace) IsAnAPIObject() {}
|
||||
func (*NamespaceList) IsAnAPIObject() {}
|
||||
func (*Secret) IsAnAPIObject() {}
|
||||
func (*SecretList) IsAnAPIObject() {}
|
||||
func (*ServiceAccount) IsAnAPIObject() {}
|
||||
func (*ServiceAccountList) IsAnAPIObject() {}
|
||||
func (*PersistentVolume) IsAnAPIObject() {}
|
||||
func (*PersistentVolumeList) IsAnAPIObject() {}
|
||||
func (*PersistentVolumeClaim) IsAnAPIObject() {}
|
||||
|
@ -756,6 +756,8 @@ type Pod struct {
|
||||
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"`
|
||||
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"`
|
||||
CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"`
|
||||
// ServiceAccount is the name of the ServiceAccount to use to run this pod
|
||||
ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"`
|
||||
// NodeSelector is a selector which must be true for the pod to fit on a node
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"`
|
||||
}
|
||||
@ -787,10 +789,11 @@ type ReplicationController struct {
|
||||
//
|
||||
// http://docs.k8s.io/replication-controller.md#pod-template
|
||||
type PodTemplate struct {
|
||||
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"`
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"`
|
||||
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"`
|
||||
Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"`
|
||||
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"`
|
||||
ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"`
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"`
|
||||
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"`
|
||||
Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"`
|
||||
}
|
||||
|
||||
// Session Affinity Type string
|
||||
@ -891,6 +894,24 @@ type ServicePort struct {
|
||||
ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"`
|
||||
}
|
||||
|
||||
// ServiceAccount binds together:
|
||||
// * a name, understood by users, and perhaps by peripheral systems, for an identity
|
||||
// * a principal that can be authenticated and authorized
|
||||
// * a set of secrets
|
||||
type ServiceAccount struct {
|
||||
TypeMeta `json:",inline"`
|
||||
|
||||
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
|
||||
Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
}
|
||||
|
||||
// ServiceAccountList is a list of ServiceAccount objects
|
||||
type ServiceAccountList struct {
|
||||
TypeMeta `json:",inline"`
|
||||
|
||||
Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"`
|
||||
}
|
||||
|
||||
// EndpointObjectReference is a reference to an object exposing the endpoint
|
||||
type EndpointObjectReference struct {
|
||||
Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"`
|
||||
@ -1692,7 +1713,23 @@ const MaxSecretSize = 1 * 1024 * 1024
|
||||
type SecretType string
|
||||
|
||||
const (
|
||||
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data
|
||||
// SecretTypeOpaque is the default; arbitrary user-defined data
|
||||
SecretTypeOpaque SecretType = "Opaque"
|
||||
|
||||
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
|
||||
//
|
||||
// Required fields:
|
||||
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
|
||||
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
|
||||
// - Secret.Data["token"] - a token that identifies the service account to the API
|
||||
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
||||
|
||||
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
||||
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
|
||||
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountTokenKey = "token"
|
||||
)
|
||||
|
||||
type SecretList struct {
|
||||
|
@ -128,6 +128,19 @@ func init() {
|
||||
// If one of the conversion functions is malformed, detect it immediately.
|
||||
panic(err)
|
||||
}
|
||||
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "ServiceAccount",
|
||||
func(label, value string) (string, string, error) {
|
||||
switch label {
|
||||
case "metadata.name":
|
||||
return label, value, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("field label not supported: %s", label)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
// If one of the conversion functions is malformed, detect it immediately.
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func convert_v1beta3_Container_To_api_Container(in *Container, out *newer.Container, s conversion.Scope) error {
|
||||
|
@ -2639,6 +2639,7 @@ func convert_v1beta3_PodSpec_To_api_PodSpec(in *PodSpec, out *newer.PodSpec, s c
|
||||
} else {
|
||||
out.NodeSelector = nil
|
||||
}
|
||||
out.ServiceAccount = in.ServiceAccount
|
||||
out.Host = in.Host
|
||||
out.HostNetwork = in.HostNetwork
|
||||
return nil
|
||||
@ -2684,6 +2685,7 @@ func convert_api_PodSpec_To_v1beta3_PodSpec(in *newer.PodSpec, out *PodSpec, s c
|
||||
} else {
|
||||
out.NodeSelector = nil
|
||||
}
|
||||
out.ServiceAccount = in.ServiceAccount
|
||||
out.Host = in.Host
|
||||
out.HostNetwork = in.HostNetwork
|
||||
return nil
|
||||
@ -3625,6 +3627,98 @@ func convert_api_Service_To_v1beta3_Service(in *newer.Service, out *Service, s c
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_v1beta3_ServiceAccount_To_api_ServiceAccount(in *ServiceAccount, out *newer.ServiceAccount, s conversion.Scope) error {
|
||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||
defaulting.(func(*ServiceAccount))(in)
|
||||
}
|
||||
if err := convert_v1beta3_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := convert_v1beta3_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Secrets != nil {
|
||||
out.Secrets = make([]newer.ObjectReference, len(in.Secrets))
|
||||
for i := range in.Secrets {
|
||||
if err := convert_v1beta3_ObjectReference_To_api_ObjectReference(&in.Secrets[i], &out.Secrets[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Secrets = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_api_ServiceAccount_To_v1beta3_ServiceAccount(in *newer.ServiceAccount, out *ServiceAccount, s conversion.Scope) error {
|
||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||
defaulting.(func(*newer.ServiceAccount))(in)
|
||||
}
|
||||
if err := convert_api_TypeMeta_To_v1beta3_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := convert_api_ObjectMeta_To_v1beta3_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Secrets != nil {
|
||||
out.Secrets = make([]ObjectReference, len(in.Secrets))
|
||||
for i := range in.Secrets {
|
||||
if err := convert_api_ObjectReference_To_v1beta3_ObjectReference(&in.Secrets[i], &out.Secrets[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Secrets = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_v1beta3_ServiceAccountList_To_api_ServiceAccountList(in *ServiceAccountList, out *newer.ServiceAccountList, s conversion.Scope) error {
|
||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||
defaulting.(func(*ServiceAccountList))(in)
|
||||
}
|
||||
if err := convert_v1beta3_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := convert_v1beta3_ListMeta_To_api_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Items != nil {
|
||||
out.Items = make([]newer.ServiceAccount, len(in.Items))
|
||||
for i := range in.Items {
|
||||
if err := convert_v1beta3_ServiceAccount_To_api_ServiceAccount(&in.Items[i], &out.Items[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Items = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_api_ServiceAccountList_To_v1beta3_ServiceAccountList(in *newer.ServiceAccountList, out *ServiceAccountList, s conversion.Scope) error {
|
||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||
defaulting.(func(*newer.ServiceAccountList))(in)
|
||||
}
|
||||
if err := convert_api_TypeMeta_To_v1beta3_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := convert_api_ListMeta_To_v1beta3_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Items != nil {
|
||||
out.Items = make([]ServiceAccount, len(in.Items))
|
||||
for i := range in.Items {
|
||||
if err := convert_api_ServiceAccount_To_v1beta3_ServiceAccount(&in.Items[i], &out.Items[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Items = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_v1beta3_ServiceList_To_api_ServiceList(in *ServiceList, out *newer.ServiceList, s conversion.Scope) error {
|
||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||
defaulting.(func(*ServiceList))(in)
|
||||
@ -4244,6 +4338,8 @@ func init() {
|
||||
convert_api_Secret_To_v1beta3_Secret,
|
||||
convert_api_SecurityContext_To_v1beta3_SecurityContext,
|
||||
convert_api_SerializedReference_To_v1beta3_SerializedReference,
|
||||
convert_api_ServiceAccountList_To_v1beta3_ServiceAccountList,
|
||||
convert_api_ServiceAccount_To_v1beta3_ServiceAccount,
|
||||
convert_api_ServiceList_To_v1beta3_ServiceList,
|
||||
convert_api_ServicePort_To_v1beta3_ServicePort,
|
||||
convert_api_ServiceSpec_To_v1beta3_ServiceSpec,
|
||||
@ -4350,6 +4446,8 @@ func init() {
|
||||
convert_v1beta3_Secret_To_api_Secret,
|
||||
convert_v1beta3_SecurityContext_To_api_SecurityContext,
|
||||
convert_v1beta3_SerializedReference_To_api_SerializedReference,
|
||||
convert_v1beta3_ServiceAccountList_To_api_ServiceAccountList,
|
||||
convert_v1beta3_ServiceAccount_To_api_ServiceAccount,
|
||||
convert_v1beta3_ServiceList_To_api_ServiceList,
|
||||
convert_v1beta3_ServicePort_To_api_ServicePort,
|
||||
convert_v1beta3_ServiceSpec_To_api_ServiceSpec,
|
||||
|
@ -52,6 +52,8 @@ func init() {
|
||||
&NamespaceList{},
|
||||
&Secret{},
|
||||
&SecretList{},
|
||||
&ServiceAccount{},
|
||||
&ServiceAccountList{},
|
||||
&PersistentVolume{},
|
||||
&PersistentVolumeList{},
|
||||
&PersistentVolumeClaim{},
|
||||
@ -97,6 +99,8 @@ func (*Namespace) IsAnAPIObject() {}
|
||||
func (*NamespaceList) IsAnAPIObject() {}
|
||||
func (*Secret) IsAnAPIObject() {}
|
||||
func (*SecretList) IsAnAPIObject() {}
|
||||
func (*ServiceAccount) IsAnAPIObject() {}
|
||||
func (*ServiceAccountList) IsAnAPIObject() {}
|
||||
func (*PersistentVolume) IsAnAPIObject() {}
|
||||
func (*PersistentVolumeList) IsAnAPIObject() {}
|
||||
func (*PersistentVolumeClaim) IsAnAPIObject() {}
|
||||
|
@ -816,6 +816,9 @@ type PodSpec struct {
|
||||
// NodeSelector is a selector which must be true for the pod to fit on a node
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"`
|
||||
|
||||
// ServiceAccount is the name of the ServiceAccount to use to run this pod
|
||||
ServiceAccount string `json:"serviceAccount" description:"name of the ServiceAccount to use to run this pod"`
|
||||
|
||||
// Host is a request to schedule this pod onto a specific host. If it is non-empty,
|
||||
// the the scheduler simply schedules this pod onto that host, assuming that it fits
|
||||
// resource requirements.
|
||||
@ -1036,6 +1039,26 @@ type ServiceList struct {
|
||||
Items []Service `json:"items" description:"list of services"`
|
||||
}
|
||||
|
||||
// ServiceAccount binds together:
|
||||
// * a name, understood by users, and perhaps by peripheral systems, for an identity
|
||||
// * a principal that can be authenticated and authorized
|
||||
// * a set of secrets
|
||||
type ServiceAccount struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://docs.k8s.io/api-conventions.md#metadata"`
|
||||
|
||||
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
|
||||
Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
}
|
||||
|
||||
// ServiceAccountList is a list of ServiceAccount objects
|
||||
type ServiceAccountList struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"`
|
||||
|
||||
Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"`
|
||||
}
|
||||
|
||||
// Endpoints is a collection of endpoints that implement the actual service. Example:
|
||||
// Name: "mysvc",
|
||||
// Subsets: [
|
||||
@ -1708,7 +1731,23 @@ const MaxSecretSize = 1 * 1024 * 1024
|
||||
type SecretType string
|
||||
|
||||
const (
|
||||
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data
|
||||
// SecretTypeOpaque is the default; arbitrary user-defined data
|
||||
SecretTypeOpaque SecretType = "Opaque"
|
||||
|
||||
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
|
||||
//
|
||||
// Required fields:
|
||||
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
|
||||
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
|
||||
// - Secret.Data["token"] - a token that identifies the service account to the API
|
||||
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
||||
|
||||
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
||||
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
|
||||
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountTokenKey = "token"
|
||||
)
|
||||
|
||||
type SecretList struct {
|
||||
|
@ -153,6 +153,13 @@ func ValidateSecretName(name string, prefix bool) (bool, string) {
|
||||
return nameIsDNSSubdomain(name, prefix)
|
||||
}
|
||||
|
||||
// ValidateServiceAccountName can be used to check whether the given service account name is valid.
|
||||
// Prefix indicates this name will be used as part of generation, in which case
|
||||
// trailing dashes are allowed.
|
||||
func ValidateServiceAccountName(name string, prefix bool) (bool, string) {
|
||||
return nameIsDNSSubdomain(name, prefix)
|
||||
}
|
||||
|
||||
// ValidateEndpointsName can be used to check whether the given endpoints name is valid.
|
||||
// Prefix indicates this name will be used as part of generation, in which case
|
||||
// trailing dashes are allowed.
|
||||
@ -1227,6 +1234,21 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList {
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateServiceAccount tests if required fields in the ServiceAccount are set.
|
||||
func ValidateServiceAccount(serviceAccount *api.ServiceAccount) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
allErrs = append(allErrs, ValidateObjectMeta(&serviceAccount.ObjectMeta, true, ValidateServiceAccountName).Prefix("metadata")...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateServiceAccountUpdate tests if required fields in the ServiceAccount are set.
|
||||
func ValidateServiceAccountUpdate(oldServiceAccount, newServiceAccount *api.ServiceAccount) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldServiceAccount.ObjectMeta, &newServiceAccount.ObjectMeta).Prefix("metadata")...)
|
||||
allErrs = append(allErrs, ValidateServiceAccount(newServiceAccount)...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateSecret tests if required fields in the Secret are set.
|
||||
func ValidateSecret(secret *api.Secret) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
@ -1246,6 +1268,12 @@ func ValidateSecret(secret *api.Secret) errs.ValidationErrorList {
|
||||
}
|
||||
|
||||
switch secret.Type {
|
||||
case api.SecretTypeServiceAccountToken:
|
||||
// Only require Annotations[kubernetes.io/service-account.name]
|
||||
// Additional fields (like Annotations[kubernetes.io/service-account.uid] and Data[token]) might be contributed later by a controller loop
|
||||
if value := secret.Annotations[api.ServiceAccountNameKey]; len(value) == 0 {
|
||||
allErrs = append(allErrs, errs.NewFieldRequired(fmt.Sprintf("metadata.annotations[%s]", api.ServiceAccountNameKey)))
|
||||
}
|
||||
case api.SecretTypeOpaque, "":
|
||||
// no-op
|
||||
default:
|
||||
|
@ -2961,6 +2961,7 @@ func TestValidateNamespaceUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateSecret(t *testing.T) {
|
||||
// Opaque secret validation
|
||||
validSecret := func() api.Secret {
|
||||
return api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
|
||||
@ -2988,6 +2989,32 @@ func TestValidateSecret(t *testing.T) {
|
||||
}
|
||||
invalidKey.Data["a..b"] = []byte("whoops")
|
||||
|
||||
// kubernetes.io/service-account-token secret validation
|
||||
validServiceAccountTokenSecret := func() api.Secret {
|
||||
return api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: "foo",
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{
|
||||
"data-1": []byte("bar"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
emptyTokenAnnotation = validServiceAccountTokenSecret()
|
||||
missingTokenAnnotation = validServiceAccountTokenSecret()
|
||||
missingTokenAnnotations = validServiceAccountTokenSecret()
|
||||
)
|
||||
emptyTokenAnnotation.Annotations[api.ServiceAccountNameKey] = ""
|
||||
delete(missingTokenAnnotation.Annotations, api.ServiceAccountNameKey)
|
||||
missingTokenAnnotations.Annotations = nil
|
||||
|
||||
tests := map[string]struct {
|
||||
secret api.Secret
|
||||
valid bool
|
||||
@ -2999,6 +3026,11 @@ func TestValidateSecret(t *testing.T) {
|
||||
"invalid namespace": {invalidNs, false},
|
||||
"over max size": {overMaxSize, false},
|
||||
"invalid key": {invalidKey, false},
|
||||
|
||||
"valid service-account-token secret": {validServiceAccountTokenSecret(), true},
|
||||
"empty service-account-token annotation": {emptyTokenAnnotation, false},
|
||||
"missing service-account-token annotation": {missingTokenAnnotation, false},
|
||||
"missing service-account-token annotations": {missingTokenAnnotations, false},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
|
@ -17,8 +17,12 @@ limitations under the License.
|
||||
package apiserver
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/basicauth"
|
||||
@ -28,7 +32,7 @@ import (
|
||||
)
|
||||
|
||||
// NewAuthenticator returns an authenticator.Request or an error
|
||||
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authenticator.Request, error) {
|
||||
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyFile string, serviceAccountLookup bool, client client.Interface) (authenticator.Request, error) {
|
||||
var authenticators []authenticator.Request
|
||||
|
||||
if len(basicAuthFile) > 0 {
|
||||
@ -55,6 +59,14 @@ func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authentica
|
||||
authenticators = append(authenticators, tokenAuth)
|
||||
}
|
||||
|
||||
if len(serviceAccountKeyFile) > 0 {
|
||||
serviceAccountAuth, err := newServiceAccountAuthenticator(serviceAccountKeyFile, serviceAccountLookup, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authenticators = append(authenticators, serviceAccountAuth)
|
||||
}
|
||||
|
||||
switch len(authenticators) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
@ -85,6 +97,16 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request,
|
||||
return bearertoken.New(tokenAuthenticator), nil
|
||||
}
|
||||
|
||||
// newServiceAccountAuthenticator returns an authenticator.Request or an error
|
||||
func newServiceAccountAuthenticator(keyfile string, lookup bool, client client.Interface) (authenticator.Request, error) {
|
||||
publicKey, err := serviceaccount.ReadPublicKey(keyfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{publicKey}, lookup, client)
|
||||
return bearertoken.New(tokenAuthenticator), nil
|
||||
}
|
||||
|
||||
// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error
|
||||
func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Request, error) {
|
||||
roots, err := util.CertPoolFromFile(clientCAFile)
|
||||
|
@ -50,6 +50,12 @@ type Authorizer interface {
|
||||
Authorize(a Attributes) (err error)
|
||||
}
|
||||
|
||||
type AuthorizerFunc func(a Attributes) error
|
||||
|
||||
func (f AuthorizerFunc) Authorize(a Attributes) error {
|
||||
return f(a)
|
||||
}
|
||||
|
||||
// AttributesRecord implements Attributes interface.
|
||||
type AttributesRecord struct {
|
||||
User user.Info
|
||||
|
@ -39,6 +39,7 @@ type Interface interface {
|
||||
EventNamespacer
|
||||
LimitRangesNamespacer
|
||||
ResourceQuotasNamespacer
|
||||
ServiceAccountsNamespacer
|
||||
SecretsNamespacer
|
||||
NamespacesInterface
|
||||
PersistentVolumesInterface
|
||||
@ -77,6 +78,10 @@ func (c *Client) ResourceQuotas(namespace string) ResourceQuotaInterface {
|
||||
return newResourceQuotas(c, namespace)
|
||||
}
|
||||
|
||||
func (c *Client) ServiceAccounts(namespace string) ServiceAccountsInterface {
|
||||
return newServiceAccounts(c, namespace)
|
||||
}
|
||||
|
||||
func (c *Client) Secrets(namespace string) SecretsInterface {
|
||||
return newSecrets(c, namespace)
|
||||
}
|
||||
|
@ -310,6 +310,9 @@ var fieldMappings = versionToResourceToFieldMapping{
|
||||
"secrets": clientFieldNameToAPIVersionFieldName{
|
||||
SecretType: "type",
|
||||
},
|
||||
"serviceAccounts": clientFieldNameToAPIVersionFieldName{
|
||||
ObjectNameField: "name",
|
||||
},
|
||||
},
|
||||
"v1beta2": resourceTypeToFieldMapping{
|
||||
"nodes": clientFieldNameToAPIVersionFieldName{
|
||||
@ -326,6 +329,9 @@ var fieldMappings = versionToResourceToFieldMapping{
|
||||
"secrets": clientFieldNameToAPIVersionFieldName{
|
||||
SecretType: "type",
|
||||
},
|
||||
"serviceAccounts": clientFieldNameToAPIVersionFieldName{
|
||||
ObjectNameField: "name",
|
||||
},
|
||||
},
|
||||
"v1beta3": resourceTypeToFieldMapping{
|
||||
"nodes": clientFieldNameToAPIVersionFieldName{
|
||||
@ -342,6 +348,9 @@ var fieldMappings = versionToResourceToFieldMapping{
|
||||
"secrets": clientFieldNameToAPIVersionFieldName{
|
||||
SecretType: "type",
|
||||
},
|
||||
"serviceAccounts": clientFieldNameToAPIVersionFieldName{
|
||||
ObjectNameField: "metadata.name",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
125
pkg/client/service_accounts.go
Normal file
125
pkg/client/service_accounts.go
Normal file
@ -0,0 +1,125 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
type ServiceAccountsNamespacer interface {
|
||||
ServiceAccounts(namespace string) ServiceAccountsInterface
|
||||
}
|
||||
|
||||
type ServiceAccountsInterface interface {
|
||||
Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error)
|
||||
Update(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error)
|
||||
Delete(name string) error
|
||||
List(label labels.Selector, field fields.Selector) (*api.ServiceAccountList, error)
|
||||
Get(name string) (*api.ServiceAccount, error)
|
||||
Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error)
|
||||
}
|
||||
|
||||
// serviceAccounts implements ServiceAccounts interface
|
||||
type serviceAccounts struct {
|
||||
client *Client
|
||||
namespace string
|
||||
}
|
||||
|
||||
// newServiceAccounts returns a new serviceAccounts object.
|
||||
func newServiceAccounts(c *Client, ns string) ServiceAccountsInterface {
|
||||
return &serviceAccounts{
|
||||
client: c,
|
||||
namespace: ns,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceAccounts) Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) {
|
||||
result := &api.ServiceAccount{}
|
||||
err := s.client.Post().
|
||||
Namespace(s.namespace).
|
||||
Resource("serviceAccounts").
|
||||
Body(serviceAccount).
|
||||
Do().
|
||||
Into(result)
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// List returns a list of serviceAccounts matching the selectors.
|
||||
func (s *serviceAccounts) List(label labels.Selector, field fields.Selector) (*api.ServiceAccountList, error) {
|
||||
result := &api.ServiceAccountList{}
|
||||
|
||||
err := s.client.Get().
|
||||
Namespace(s.namespace).
|
||||
Resource("serviceAccounts").
|
||||
LabelsSelectorParam(label).
|
||||
FieldsSelectorParam(field).
|
||||
Do().
|
||||
Into(result)
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Get returns the given serviceAccount, or an error.
|
||||
func (s *serviceAccounts) Get(name string) (*api.ServiceAccount, error) {
|
||||
result := &api.ServiceAccount{}
|
||||
err := s.client.Get().
|
||||
Namespace(s.namespace).
|
||||
Resource("serviceAccounts").
|
||||
Name(name).
|
||||
Do().
|
||||
Into(result)
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Watch starts watching for serviceAccounts matching the given selectors.
|
||||
func (s *serviceAccounts) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
|
||||
return s.client.Get().
|
||||
Prefix("watch").
|
||||
Namespace(s.namespace).
|
||||
Resource("serviceAccounts").
|
||||
Param("resourceVersion", resourceVersion).
|
||||
LabelsSelectorParam(label).
|
||||
FieldsSelectorParam(field).
|
||||
Watch()
|
||||
}
|
||||
|
||||
func (s *serviceAccounts) Delete(name string) error {
|
||||
return s.client.Delete().
|
||||
Namespace(s.namespace).
|
||||
Resource("serviceAccounts").
|
||||
Name(name).
|
||||
Do().
|
||||
Error()
|
||||
}
|
||||
|
||||
func (s *serviceAccounts) Update(serviceAccount *api.ServiceAccount) (result *api.ServiceAccount, err error) {
|
||||
result = &api.ServiceAccount{}
|
||||
err = s.client.Put().
|
||||
Namespace(s.namespace).
|
||||
Resource("serviceAccounts").
|
||||
Name(serviceAccount.Name).
|
||||
Body(serviceAccount).
|
||||
Do().
|
||||
Into(result)
|
||||
|
||||
return
|
||||
}
|
61
pkg/client/testclient/fake_service_accounts.go
Normal file
61
pkg/client/testclient/fake_service_accounts.go
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package testclient
|
||||
|
||||
import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
// FakeServiceAccounts implements ServiceAccountsInterface. Meant to be embedded into a struct to get a default
|
||||
// implementation. This makes faking out just the method you want to test easier.
|
||||
type FakeServiceAccounts struct {
|
||||
Fake *Fake
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func (c *FakeServiceAccounts) List(labels labels.Selector, field fields.Selector) (*api.ServiceAccountList, error) {
|
||||
obj, err := c.Fake.Invokes(FakeAction{Action: "list-serviceaccounts"}, &api.ServiceAccountList{})
|
||||
return obj.(*api.ServiceAccountList), err
|
||||
}
|
||||
|
||||
func (c *FakeServiceAccounts) Get(name string) (*api.ServiceAccount, error) {
|
||||
obj, err := c.Fake.Invokes(FakeAction{Action: "get-serviceaccount", Value: name}, &api.ServiceAccount{})
|
||||
return obj.(*api.ServiceAccount), err
|
||||
}
|
||||
|
||||
func (c *FakeServiceAccounts) Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) {
|
||||
obj, err := c.Fake.Invokes(FakeAction{Action: "create-serviceaccount", Value: serviceAccount}, &api.ServiceAccount{})
|
||||
return obj.(*api.ServiceAccount), err
|
||||
}
|
||||
|
||||
func (c *FakeServiceAccounts) Update(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) {
|
||||
obj, err := c.Fake.Invokes(FakeAction{Action: "update-serviceaccount", Value: serviceAccount}, &api.ServiceAccount{})
|
||||
return obj.(*api.ServiceAccount), err
|
||||
}
|
||||
|
||||
func (c *FakeServiceAccounts) Delete(name string) error {
|
||||
_, err := c.Fake.Invokes(FakeAction{Action: "delete-serviceaccount", Value: name}, &api.ServiceAccount{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *FakeServiceAccounts) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
|
||||
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-serviceAccounts", Value: resourceVersion})
|
||||
return c.Fake.Watch, c.Fake.Err
|
||||
}
|
@ -107,6 +107,10 @@ func (c *Fake) Services(namespace string) client.ServiceInterface {
|
||||
return &FakeServices{Fake: c, Namespace: namespace}
|
||||
}
|
||||
|
||||
func (c *Fake) ServiceAccounts(namespace string) client.ServiceAccountsInterface {
|
||||
return &FakeServiceAccounts{Fake: c, Namespace: namespace}
|
||||
}
|
||||
|
||||
func (c *Fake) Secrets(namespace string) client.SecretsInterface {
|
||||
return &FakeSecrets{Fake: c, Namespace: namespace}
|
||||
}
|
||||
|
@ -228,3 +228,69 @@ func NewInformer(
|
||||
}
|
||||
return clientState, New(cfg)
|
||||
}
|
||||
|
||||
// NewIndexerInformer returns a cache.Indexer and a controller for populating the index
|
||||
// while also providing event notifications. You should only used the returned
|
||||
// cache.Index for Get/List operations; Add/Modify/Deletes will cause the event
|
||||
// notifications to be faulty.
|
||||
//
|
||||
// Parameters:
|
||||
// * lw is list and watch functions for the source of the resource you want to
|
||||
// be informed of.
|
||||
// * objType is an object of the type that you expect to receive.
|
||||
// * resyncPeriod: if non-zero, will re-list this often (you will get OnUpdate
|
||||
// calls, even if nothing changed). Otherwise, re-list will be delayed as
|
||||
// long as possible (until the upstream source closes the watch or times out,
|
||||
// or you stop the controller).
|
||||
// * h is the object you want notifications sent to.
|
||||
//
|
||||
func NewIndexerInformer(
|
||||
lw cache.ListerWatcher,
|
||||
objType runtime.Object,
|
||||
resyncPeriod time.Duration,
|
||||
h ResourceEventHandler,
|
||||
indexers cache.Indexers,
|
||||
) (cache.Indexer, *Controller) {
|
||||
// This will hold the client state, as we know it.
|
||||
clientState := cache.NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers)
|
||||
|
||||
// This will hold incoming changes. Note how we pass clientState in as a
|
||||
// KeyLister, that way resync operations will result in the correct set
|
||||
// of update/delete deltas.
|
||||
fifo := cache.NewDeltaFIFO(cache.MetaNamespaceKeyFunc, nil, clientState)
|
||||
|
||||
cfg := &Config{
|
||||
Queue: fifo,
|
||||
ListerWatcher: lw,
|
||||
ObjectType: objType,
|
||||
FullResyncPeriod: resyncPeriod,
|
||||
RetryOnError: false,
|
||||
|
||||
Process: func(obj interface{}) error {
|
||||
// from oldest to newest
|
||||
for _, d := range obj.(cache.Deltas) {
|
||||
switch d.Type {
|
||||
case cache.Sync, cache.Added, cache.Updated:
|
||||
if old, exists, err := clientState.Get(d.Object); err == nil && exists {
|
||||
if err := clientState.Update(d.Object); err != nil {
|
||||
return err
|
||||
}
|
||||
h.OnUpdate(old, d.Object)
|
||||
} else {
|
||||
if err := clientState.Add(d.Object); err != nil {
|
||||
return err
|
||||
}
|
||||
h.OnAdd(d.Object)
|
||||
}
|
||||
case cache.Deleted:
|
||||
if err := clientState.Delete(d.Object); err != nil {
|
||||
return err
|
||||
}
|
||||
h.OnDelete(d.Object)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return clientState, New(cfg)
|
||||
}
|
||||
|
@ -64,7 +64,9 @@ func describerMap(c *client.Client) map[string]Describer {
|
||||
m := map[string]Describer{
|
||||
"Pod": &PodDescriber{c},
|
||||
"ReplicationController": &ReplicationControllerDescriber{c},
|
||||
"Secret": &SecretDescriber{c},
|
||||
"Service": &ServiceDescriber{c},
|
||||
"ServiceAccount": &ServiceAccountDescriber{c},
|
||||
"Minion": &NodeDescriber{c},
|
||||
"Node": &NodeDescriber{c},
|
||||
"LimitRange": &LimitRangeDescriber{c},
|
||||
@ -421,6 +423,44 @@ func describeReplicationController(controller *api.ReplicationController, events
|
||||
})
|
||||
}
|
||||
|
||||
// SecretDescriber generates information about a secret
|
||||
type SecretDescriber struct {
|
||||
client.Interface
|
||||
}
|
||||
|
||||
func (d *SecretDescriber) Describe(namespace, name string) (string, error) {
|
||||
c := d.Secrets(namespace)
|
||||
|
||||
secret, err := c.Get(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return describeSecret(secret)
|
||||
}
|
||||
|
||||
func describeSecret(secret *api.Secret) (string, error) {
|
||||
return tabbedString(func(out io.Writer) error {
|
||||
fmt.Fprintf(out, "Name:\t%s\n", secret.Name)
|
||||
fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(secret.Labels))
|
||||
fmt.Fprintf(out, "Annotations:\t%s\n", formatLabels(secret.Annotations))
|
||||
|
||||
fmt.Fprintf(out, "\nType:\t%s\n", secret.Type)
|
||||
|
||||
fmt.Fprintf(out, "\nData\n====\n")
|
||||
for k, v := range secret.Data {
|
||||
switch {
|
||||
case k == api.ServiceAccountTokenKey && secret.Type == api.SecretTypeServiceAccountToken:
|
||||
fmt.Fprintf(out, "%s:\t%s\n", k, string(v))
|
||||
default:
|
||||
fmt.Fprintf(out, "%s:\t%d bytes\n", k, len(v))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ServiceDescriber generates information about a service.
|
||||
type ServiceDescriber struct {
|
||||
client.Interface
|
||||
@ -471,6 +511,67 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api
|
||||
})
|
||||
}
|
||||
|
||||
// ServiceAccountDescriber generates information about a service.
|
||||
type ServiceAccountDescriber struct {
|
||||
client.Interface
|
||||
}
|
||||
|
||||
func (d *ServiceAccountDescriber) Describe(namespace, name string) (string, error) {
|
||||
c := d.ServiceAccounts(namespace)
|
||||
|
||||
serviceAccount, err := c.Get(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tokens := []api.Secret{}
|
||||
|
||||
tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
|
||||
secrets, err := d.Secrets(namespace).List(labels.Everything(), tokenSelector)
|
||||
if err == nil {
|
||||
for _, s := range secrets.Items {
|
||||
name, _ := s.Annotations[api.ServiceAccountNameKey]
|
||||
uid, _ := s.Annotations[api.ServiceAccountUIDKey]
|
||||
if name == serviceAccount.Name && uid == string(serviceAccount.UID) {
|
||||
tokens = append(tokens, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return describeServiceAccount(serviceAccount, tokens)
|
||||
}
|
||||
|
||||
func describeServiceAccount(serviceAccount *api.ServiceAccount, tokens []api.Secret) (string, error) {
|
||||
return tabbedString(func(out io.Writer) error {
|
||||
fmt.Fprintf(out, "Name:\t%s\n", serviceAccount.Name)
|
||||
fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(serviceAccount.Labels))
|
||||
|
||||
if len(serviceAccount.Secrets) == 0 {
|
||||
fmt.Fprintf(out, "Secrets:\t<none>\n")
|
||||
} else {
|
||||
prefix := "Secrets:"
|
||||
for _, s := range serviceAccount.Secrets {
|
||||
fmt.Fprintf(out, "%s\t%s\n", prefix, s)
|
||||
prefix = " "
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
|
||||
if len(tokens) == 0 {
|
||||
fmt.Fprintf(out, "Tokens: \t<none>\n")
|
||||
} else {
|
||||
prefix := "Tokens: "
|
||||
for _, t := range tokens {
|
||||
fmt.Fprintf(out, "%s\t%s\n", prefix, t.Name)
|
||||
prefix = " "
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// NodeDescriber generates information about a node.
|
||||
type NodeDescriber struct {
|
||||
client.Interface
|
||||
|
@ -255,6 +255,7 @@ var limitRangeColumns = []string{"NAME"}
|
||||
var resourceQuotaColumns = []string{"NAME"}
|
||||
var namespaceColumns = []string{"NAME", "LABELS", "STATUS"}
|
||||
var secretColumns = []string{"NAME", "TYPE", "DATA"}
|
||||
var serviceAccountColumns = []string{"NAME", "SECRETS"}
|
||||
var persistentVolumeColumns = []string{"NAME", "LABELS", "CAPACITY", "ACCESSMODES", "STATUS", "CLAIM"}
|
||||
var persistentVolumeClaimColumns = []string{"NAME", "LABELS", "STATUS", "VOLUME"}
|
||||
var componentStatusColumns = []string{"NAME", "STATUS", "MESSAGE", "ERROR"}
|
||||
@ -283,6 +284,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() {
|
||||
h.Handler(namespaceColumns, printNamespaceList)
|
||||
h.Handler(secretColumns, printSecret)
|
||||
h.Handler(secretColumns, printSecretList)
|
||||
h.Handler(serviceAccountColumns, printServiceAccount)
|
||||
h.Handler(serviceAccountColumns, printServiceAccountList)
|
||||
h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaim)
|
||||
h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaimList)
|
||||
h.Handler(persistentVolumeColumns, printPersistentVolume)
|
||||
@ -596,6 +599,21 @@ func printSecretList(list *api.SecretList, w io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func printServiceAccount(item *api.ServiceAccount, w io.Writer) error {
|
||||
_, err := fmt.Fprintf(w, "%s\t%d\n", item.Name, len(item.Secrets))
|
||||
return err
|
||||
}
|
||||
|
||||
func printServiceAccountList(list *api.ServiceAccountList, w io.Writer) error {
|
||||
for _, item := range list.Items {
|
||||
if err := printServiceAccount(&item, w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNode(node *api.Node, w io.Writer) error {
|
||||
conditionMap := make(map[api.NodeConditionType]*api.NodeCondition)
|
||||
NodeAllConditions := []api.NodeConditionType{api.NodeReady}
|
||||
|
@ -65,6 +65,7 @@ import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
||||
ipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
||||
etcdipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator/etcd"
|
||||
serviceaccountetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount/etcd"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/ui"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
@ -402,6 +403,7 @@ func (m *Master) init(c *Config) {
|
||||
|
||||
resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewStorage(c.EtcdHelper)
|
||||
secretStorage := secretetcd.NewStorage(c.EtcdHelper)
|
||||
serviceAccountStorage := serviceaccountetcd.NewStorage(c.EtcdHelper)
|
||||
persistentVolumeStorage, persistentVolumeStatusStorage := pvetcd.NewStorage(c.EtcdHelper)
|
||||
persistentVolumeClaimStorage, persistentVolumeClaimStatusStorage := pvcetcd.NewStorage(c.EtcdHelper)
|
||||
|
||||
@ -453,6 +455,7 @@ func (m *Master) init(c *Config) {
|
||||
"namespaces/status": namespaceStatusStorage,
|
||||
"namespaces/finalize": namespaceFinalizeStorage,
|
||||
"secrets": secretStorage,
|
||||
"serviceAccounts": serviceAccountStorage,
|
||||
"persistentVolumes": persistentVolumeStorage,
|
||||
"persistentVolumes/status": persistentVolumeStatusStorage,
|
||||
"persistentVolumeClaims": persistentVolumeClaimStorage,
|
||||
|
@ -107,6 +107,10 @@ func finalize(kubeClient client.Interface, namespace api.Namespace) (*api.Namesp
|
||||
|
||||
// deleteAllContent will delete all content known to the system in a namespace
|
||||
func deleteAllContent(kubeClient client.Interface, namespace string) (err error) {
|
||||
err = deleteServiceAccounts(kubeClient, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = deleteServices(kubeClient, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -217,6 +221,20 @@ func deleteResourceQuotas(kubeClient client.Interface, ns string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteServiceAccounts(kubeClient client.Interface, ns string) error {
|
||||
items, err := kubeClient.ServiceAccounts(ns).List(labels.Everything(), fields.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items.Items {
|
||||
err := kubeClient.ServiceAccounts(ns).Delete(items.Items[i].Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteServices(kubeClient client.Interface, ns string) error {
|
||||
items, err := kubeClient.Services(ns).List(labels.Everything())
|
||||
if err != nil {
|
||||
|
19
pkg/registry/serviceaccount/doc.go
Normal file
19
pkg/registry/serviceaccount/doc.go
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package serviceaccount provides a Registry interface and a strategy
|
||||
// implementation for storing ServiceAccount API objects.
|
||||
package serviceaccount
|
64
pkg/registry/serviceaccount/etcd/etcd.go
Normal file
64
pkg/registry/serviceaccount/etcd/etcd.go
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic"
|
||||
etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||
)
|
||||
|
||||
// REST implements a RESTStorage for service accounts against etcd
|
||||
type REST struct {
|
||||
*etcdgeneric.Etcd
|
||||
}
|
||||
|
||||
const Prefix = "/serviceaccounts"
|
||||
|
||||
// NewStorage returns a RESTStorage object that will work against service accounts objects.
|
||||
func NewStorage(h tools.EtcdHelper) *REST {
|
||||
store := &etcdgeneric.Etcd{
|
||||
NewFunc: func() runtime.Object { return &api.ServiceAccount{} },
|
||||
NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} },
|
||||
KeyRootFunc: func(ctx api.Context) string {
|
||||
return etcdgeneric.NamespaceKeyRootFunc(ctx, Prefix)
|
||||
},
|
||||
KeyFunc: func(ctx api.Context, name string) (string, error) {
|
||||
return etcdgeneric.NamespaceKeyFunc(ctx, Prefix, name)
|
||||
},
|
||||
ObjectNameFunc: func(obj runtime.Object) (string, error) {
|
||||
return obj.(*api.ServiceAccount).Name, nil
|
||||
},
|
||||
PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher {
|
||||
return serviceaccount.Matcher(label, field)
|
||||
},
|
||||
EndpointName: "serviceaccounts",
|
||||
|
||||
Helper: h,
|
||||
}
|
||||
|
||||
store.CreateStrategy = serviceaccount.Strategy
|
||||
store.UpdateStrategy = serviceaccount.Strategy
|
||||
store.ReturnDeletedObject = true
|
||||
|
||||
return &REST{store}
|
||||
}
|
86
pkg/registry/serviceaccount/etcd/etcd_test.go
Normal file
86
pkg/registry/serviceaccount/etcd/etcd_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools/etcdtest"
|
||||
)
|
||||
|
||||
func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) {
|
||||
fakeEtcdClient := tools.NewFakeEtcdClient(t)
|
||||
fakeEtcdClient.TestIndex = true
|
||||
helper := tools.NewEtcdHelper(fakeEtcdClient, testapi.Codec(), etcdtest.PathPrefix())
|
||||
return fakeEtcdClient, helper
|
||||
}
|
||||
|
||||
func validNewServiceAccount(name string) *api.ServiceAccount {
|
||||
return &api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: api.NamespaceDefault,
|
||||
},
|
||||
Secrets: []api.ObjectReference{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
fakeEtcdClient, helper := newHelper(t)
|
||||
storage := NewStorage(helper)
|
||||
test := resttest.New(t, storage, fakeEtcdClient.SetError)
|
||||
serviceAccount := validNewServiceAccount("foo")
|
||||
serviceAccount.Name = ""
|
||||
serviceAccount.GenerateName = "foo-"
|
||||
test.TestCreate(
|
||||
// valid
|
||||
serviceAccount,
|
||||
// invalid
|
||||
&api.ServiceAccount{},
|
||||
&api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{Name: "name with spaces"},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
fakeEtcdClient, helper := newHelper(t)
|
||||
storage := NewStorage(helper)
|
||||
test := resttest.New(t, storage, fakeEtcdClient.SetError)
|
||||
key := etcdtest.AddPrefix("serviceaccounts/default/foo")
|
||||
|
||||
fakeEtcdClient.ExpectNotFoundGet(key)
|
||||
fakeEtcdClient.ChangeIndex = 2
|
||||
serviceAccount := validNewServiceAccount("foo")
|
||||
existing := validNewServiceAccount("exists")
|
||||
obj, err := storage.Create(api.NewDefaultContext(), existing)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create object: %v", err)
|
||||
}
|
||||
older := obj.(*api.ServiceAccount)
|
||||
older.ResourceVersion = "1"
|
||||
|
||||
test.TestUpdate(
|
||||
serviceAccount,
|
||||
existing,
|
||||
older,
|
||||
)
|
||||
}
|
87
pkg/registry/serviceaccount/registry.go
Normal file
87
pkg/registry/serviceaccount/registry.go
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
// Registry is an interface implemented by things that know how to store ServiceAccount objects.
|
||||
type Registry interface {
|
||||
// ListServiceAccounts obtains a list of ServiceAccounts having labels which match selector.
|
||||
ListServiceAccounts(ctx api.Context, selector labels.Selector) (*api.ServiceAccountList, error)
|
||||
// Watch for new/changed/deleted service accounts
|
||||
WatchServiceAccounts(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error)
|
||||
// Get a specific ServiceAccount
|
||||
GetServiceAccount(ctx api.Context, name string) (*api.ServiceAccount, error)
|
||||
// Create a ServiceAccount based on a specification.
|
||||
CreateServiceAccount(ctx api.Context, ServiceAccount *api.ServiceAccount) error
|
||||
// Update an existing ServiceAccount
|
||||
UpdateServiceAccount(ctx api.Context, ServiceAccount *api.ServiceAccount) error
|
||||
// Delete an existing ServiceAccount
|
||||
DeleteServiceAccount(ctx api.Context, name string) error
|
||||
}
|
||||
|
||||
// storage puts strong typing around storage calls
|
||||
type storage struct {
|
||||
rest.StandardStorage
|
||||
}
|
||||
|
||||
// NewRegistry returns a new Registry interface for the given Storage. Any mismatched
|
||||
// types will panic.
|
||||
func NewRegistry(s rest.StandardStorage) Registry {
|
||||
return &storage{s}
|
||||
}
|
||||
|
||||
func (s *storage) ListServiceAccounts(ctx api.Context, label labels.Selector) (*api.ServiceAccountList, error) {
|
||||
obj, err := s.List(ctx, label, fields.Everything())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*api.ServiceAccountList), nil
|
||||
}
|
||||
|
||||
func (s *storage) WatchServiceAccounts(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
|
||||
return s.Watch(ctx, label, field, resourceVersion)
|
||||
}
|
||||
|
||||
func (s *storage) GetServiceAccount(ctx api.Context, name string) (*api.ServiceAccount, error) {
|
||||
obj, err := s.Get(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*api.ServiceAccount), nil
|
||||
}
|
||||
|
||||
func (s *storage) CreateServiceAccount(ctx api.Context, serviceAccount *api.ServiceAccount) error {
|
||||
_, err := s.Create(ctx, serviceAccount)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *storage) UpdateServiceAccount(ctx api.Context, serviceAccount *api.ServiceAccount) error {
|
||||
_, _, err := s.Update(ctx, serviceAccount)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *storage) DeleteServiceAccount(ctx api.Context, name string) error {
|
||||
_, err := s.Delete(ctx, name, nil)
|
||||
return err
|
||||
}
|
88
pkg/registry/serviceaccount/strategy.go
Normal file
88
pkg/registry/serviceaccount/strategy.go
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
|
||||
)
|
||||
|
||||
// strategy implements behavior for ServiceAccount objects
|
||||
type strategy struct {
|
||||
runtime.ObjectTyper
|
||||
api.NameGenerator
|
||||
}
|
||||
|
||||
// Strategy is the default logic that applies when creating and updating ServiceAccount
|
||||
// objects via the REST API.
|
||||
var Strategy = strategy{api.Scheme, api.SimpleNameGenerator}
|
||||
|
||||
func (strategy) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (strategy) PrepareForCreate(obj runtime.Object) {
|
||||
cleanSecretReferences(obj.(*api.ServiceAccount))
|
||||
}
|
||||
|
||||
func (strategy) Validate(ctx api.Context, obj runtime.Object) fielderrors.ValidationErrorList {
|
||||
return validation.ValidateServiceAccount(obj.(*api.ServiceAccount))
|
||||
}
|
||||
|
||||
func (strategy) AllowCreateOnUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (strategy) PrepareForUpdate(obj, old runtime.Object) {
|
||||
cleanSecretReferences(obj.(*api.ServiceAccount))
|
||||
}
|
||||
|
||||
func cleanSecretReferences(serviceAccount *api.ServiceAccount) {
|
||||
for i, secret := range serviceAccount.Secrets {
|
||||
serviceAccount.Secrets[i] = api.ObjectReference{Name: secret.Name}
|
||||
}
|
||||
}
|
||||
|
||||
func (strategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList {
|
||||
return validation.ValidateServiceAccountUpdate(old.(*api.ServiceAccount), obj.(*api.ServiceAccount))
|
||||
}
|
||||
|
||||
// Matcher returns a generic matcher for a given label and field selector.
|
||||
func Matcher(label labels.Selector, field fields.Selector) generic.Matcher {
|
||||
return generic.MatcherFunc(func(obj runtime.Object) (bool, error) {
|
||||
sa, ok := obj.(*api.ServiceAccount)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("not a serviceaccount")
|
||||
}
|
||||
fields := SelectableFields(sa)
|
||||
return label.Matches(labels.Set(sa.Labels)) && field.Matches(fields), nil
|
||||
})
|
||||
}
|
||||
|
||||
// SelectableFields returns a label set that represents the object
|
||||
func SelectableFields(obj *api.ServiceAccount) labels.Set {
|
||||
return labels.Set{
|
||||
"metadata.name": obj.Name,
|
||||
}
|
||||
}
|
19
pkg/serviceaccount/doc.go
Normal file
19
pkg/serviceaccount/doc.go
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package serviceaccount provides implementations
|
||||
// to manage service accounts and service account tokens
|
||||
package serviceaccount
|
228
pkg/serviceaccount/jwt.go
Normal file
228
pkg/serviceaccount/jwt.go
Normal file
@ -0,0 +1,228 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceAccountUsernamePrefix = "serviceaccount"
|
||||
ServiceAccountUsernameSeparator = ":"
|
||||
|
||||
Issuer = "kubernetes/serviceaccount"
|
||||
|
||||
SubjectClaim = "sub"
|
||||
IssuerClaim = "iss"
|
||||
ServiceAccountNameClaim = "kubernetes.io/serviceaccount/service-account.name"
|
||||
ServiceAccountUIDClaim = "kubernetes.io/serviceaccount/service-account.uid"
|
||||
SecretNameClaim = "kubernetes.io/serviceaccount/secret.name"
|
||||
NamespaceClaim = "kubernetes.io/serviceaccount/namespace"
|
||||
)
|
||||
|
||||
type TokenGenerator interface {
|
||||
// GenerateToken generates a token which will identify the given ServiceAccount.
|
||||
// The returned token will be stored in the given (and yet-unpersisted) Secret.
|
||||
GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error)
|
||||
}
|
||||
|
||||
// ReadPrivateKey is a helper function for reading an rsa.PrivateKey from a PEM-encoded file
|
||||
func ReadPrivateKey(file string) (*rsa.PrivateKey, error) {
|
||||
data, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jwt.ParseRSAPrivateKeyFromPEM(data)
|
||||
}
|
||||
|
||||
// ReadPublicKey is a helper function for reading an rsa.PublicKey from a PEM-encoded file
|
||||
// Reads public keys from both public and private key files
|
||||
func ReadPublicKey(file string) (*rsa.PublicKey, error) {
|
||||
data, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil {
|
||||
return &privateKey.PublicKey, nil
|
||||
}
|
||||
|
||||
return jwt.ParseRSAPublicKeyFromPEM(data)
|
||||
}
|
||||
|
||||
// MakeUsername generates a username from the given namespace and ServiceAccount name.
|
||||
// The resulting username can be passed to SplitUsername to extract the original namespace and ServiceAccount name.
|
||||
func MakeUsername(namespace, name string) string {
|
||||
return strings.Join([]string{ServiceAccountUsernamePrefix, namespace, name}, ServiceAccountUsernameSeparator)
|
||||
}
|
||||
|
||||
// SplitUsername returns the namespace and ServiceAccount name embedded in the given username,
|
||||
// or an error if the username is not a valid name produced by MakeUsername
|
||||
func SplitUsername(username string) (string, string, error) {
|
||||
parts := strings.Split(username, ServiceAccountUsernameSeparator)
|
||||
if len(parts) != 3 || parts[0] != ServiceAccountUsernamePrefix || len(parts[1]) == 0 || len(parts[2]) == 0 {
|
||||
return "", "", fmt.Errorf("Username must be in the form %s", MakeUsername("namespace", "name"))
|
||||
}
|
||||
return parts[1], parts[2], nil
|
||||
}
|
||||
|
||||
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.
|
||||
// privateKey is a PEM-encoded byte array of a private RSA key.
|
||||
// JWTTokenAuthenticator()
|
||||
func JWTTokenGenerator(key *rsa.PrivateKey) TokenGenerator {
|
||||
return &jwtTokenGenerator{key}
|
||||
}
|
||||
|
||||
type jwtTokenGenerator struct {
|
||||
key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (j *jwtTokenGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
|
||||
token := jwt.New(jwt.SigningMethodRS256)
|
||||
|
||||
// Identify the issuer
|
||||
token.Claims[IssuerClaim] = Issuer
|
||||
|
||||
// Username: `serviceaccount:<namespace>:<serviceaccount>`
|
||||
token.Claims[SubjectClaim] = MakeUsername(serviceAccount.Namespace, serviceAccount.Name)
|
||||
|
||||
// Persist enough structured info for the authenticator to be able to look up the service account and secret
|
||||
token.Claims[NamespaceClaim] = serviceAccount.Namespace
|
||||
token.Claims[ServiceAccountNameClaim] = serviceAccount.Name
|
||||
token.Claims[ServiceAccountUIDClaim] = serviceAccount.UID
|
||||
token.Claims[SecretNameClaim] = secret.Name
|
||||
|
||||
// Sign and get the complete encoded token as a string
|
||||
return token.SignedString(j.key)
|
||||
}
|
||||
|
||||
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
|
||||
// Token signatures are verified using each of the given public keys until one works (allowing key rotation)
|
||||
// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified using the given client
|
||||
func JWTTokenAuthenticator(keys []*rsa.PublicKey, lookup bool, client client.Interface) authenticator.Token {
|
||||
return &jwtTokenAuthenticator{keys, lookup, client}
|
||||
}
|
||||
|
||||
type jwtTokenAuthenticator struct {
|
||||
keys []*rsa.PublicKey
|
||||
lookup bool
|
||||
client client.Interface
|
||||
}
|
||||
|
||||
func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) {
|
||||
var validationError error
|
||||
|
||||
for _, key := range j.keys {
|
||||
// Attempt to verify with each key until we find one that works
|
||||
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return key, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case *jwt.ValidationError:
|
||||
if (err.Errors & jwt.ValidationErrorMalformed) != 0 {
|
||||
// Not a JWT, no point in continuing
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
if (err.Errors & jwt.ValidationErrorSignatureInvalid) != 0 {
|
||||
// Signature error, perhaps one of the other keys will verify the signature
|
||||
// If not, we want to return this error
|
||||
validationError = err
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Other errors should just return as errors
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// If we get here, we have a token with a recognized signature
|
||||
|
||||
// Make sure we issued the token
|
||||
iss, _ := parsedToken.Claims[IssuerClaim].(string)
|
||||
if iss != Issuer {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Make sure the claims we need exist
|
||||
sub, _ := parsedToken.Claims[SubjectClaim].(string)
|
||||
if len(sub) == 0 {
|
||||
return nil, false, errors.New("sub claim is missing")
|
||||
}
|
||||
namespace, _ := parsedToken.Claims[NamespaceClaim].(string)
|
||||
if len(namespace) == 0 {
|
||||
return nil, false, errors.New("namespace claim is missing")
|
||||
}
|
||||
secretName, _ := parsedToken.Claims[SecretNameClaim].(string)
|
||||
if len(namespace) == 0 {
|
||||
return nil, false, errors.New("secretName claim is missing")
|
||||
}
|
||||
serviceAccountName, _ := parsedToken.Claims[ServiceAccountNameClaim].(string)
|
||||
if len(serviceAccountName) == 0 {
|
||||
return nil, false, errors.New("serviceAccountName claim is missing")
|
||||
}
|
||||
serviceAccountUID, _ := parsedToken.Claims[ServiceAccountUIDClaim].(string)
|
||||
if len(serviceAccountUID) == 0 {
|
||||
return nil, false, errors.New("serviceAccountUID claim is missing")
|
||||
}
|
||||
|
||||
if j.lookup {
|
||||
// Make sure token hasn't been invalidated by deletion of the secret
|
||||
secret, err := j.client.Secrets(namespace).Get(secretName)
|
||||
if err != nil {
|
||||
return nil, false, errors.New("Token has been invalidated")
|
||||
}
|
||||
if bytes.Compare(secret.Data[api.ServiceAccountTokenKey], []byte(token)) != 0 {
|
||||
return nil, false, errors.New("Token does not match server's copy")
|
||||
}
|
||||
|
||||
// Make sure service account still exists (name and UID)
|
||||
serviceAccount, err := j.client.ServiceAccounts(namespace).Get(serviceAccountName)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if string(serviceAccount.UID) != serviceAccountUID {
|
||||
return nil, false, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
|
||||
}
|
||||
}
|
||||
|
||||
return &user.DefaultInfo{
|
||||
Name: sub,
|
||||
UID: serviceAccountUID,
|
||||
Groups: []string{},
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
return nil, false, validationError
|
||||
}
|
243
pkg/serviceaccount/jwt_test.go
Normal file
243
pkg/serviceaccount/jwt_test.go
Normal file
@ -0,0 +1,243 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
const otherPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArXz0QkIG1B5Bj2/W69GH
|
||||
rsm5e+RC3kE+VTgocge0atqlLBek35tRqLgUi3AcIrBZ/0YctMSWDVcRt5fkhWwe
|
||||
Lqjj6qvAyNyOkrkBi1NFDpJBjYJtuKHgRhNxXbOzTSNpdSKXTfOkzqv56MwHOP25
|
||||
yP/NNAODUtr92D5ySI5QX8RbXW+uDn+ixul286PBW/BCrE4tuS88dA0tYJPf8LCu
|
||||
sqQOwlXYH/rNUg4Pyl9xxhR5DIJR0OzNNfChjw60zieRIt2LfM83fXhwk8IxRGkc
|
||||
gPZm7ZsipmfbZK2Tkhnpsa4QxDg7zHJPMsB5kxRXW0cQipXcC3baDyN9KBApNXa0
|
||||
PwIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
|
||||
const publicKey = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249XwEo9k4tM8fMxV7zx
|
||||
OhcrP+WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecI
|
||||
zshKuv1gKIxbbLQMOuK1eA/4HALyEkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG
|
||||
51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJU
|
||||
j7OTh/AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEi
|
||||
B4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRM
|
||||
WwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
`
|
||||
|
||||
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA249XwEo9k4tM8fMxV7zxOhcrP+WvXn917koM5Qr2ZXs4vo26
|
||||
e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecIzshKuv1gKIxbbLQMOuK1eA/4HALy
|
||||
EkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0T
|
||||
ctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh/AjjCnMnkgvKT2tpKxYQ59P
|
||||
gDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp
|
||||
1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRMWwIDAQABAoIBAHJx8GqyCBDNbqk7
|
||||
e7/hI9iE1S10Wwol5GH2RWxqX28cYMKq+8aE2LI1vPiXO89xOgelk4DN6urX6xjK
|
||||
ZBF8RRIMQy/e/O2F4+3wl+Nl4vOXV1u6iVXMsD6JRg137mqJf1Fr9elg1bsaRofL
|
||||
Q7CxPoB8dhS+Qb+hj0DhlqhgA9zG345CQCAds0ZYAZe8fP7bkwrLqZpMn7Dz9WVm
|
||||
++YgYYKjuE95kPuup/LtWfA9rJyE/Fws8/jGvRSpVn1XglMLSMKhLd27sE8ZUSV0
|
||||
2KUzbfRGE0+AnRULRrjpYaPu0XQ2JjdNvtkjBnv27RB89W9Gklxq821eH1Y8got8
|
||||
FZodjxECgYEA93pz7AQZ2xDs67d1XLCzpX84GxKzttirmyj3OIlxgzVHjEMsvw8v
|
||||
sjFiBU5xEEQDosrBdSknnlJqyiq1YwWG/WDckr13d8G2RQWoySN7JVmTQfXcLoTu
|
||||
YGRiiTuoEi3ab3ZqrgGrFgX7T/cHuasbYvzCvhM2b4VIR3aSxU2DTUMCgYEA4x7J
|
||||
T/ErP6GkU5nKstu/mIXwNzayEO1BJvPYsy7i7EsxTm3xe/b8/6cYOz5fvJLGH5mT
|
||||
Q8YvuLqBcMwZardrYcwokD55UvNLOyfADDFZ6l3WntIqbA640Ok2g1X4U8J09xIq
|
||||
ZLIWK1yWbbvi4QCeN5hvWq47e8sIj5QHjIIjRwkCgYEAyNqjltxFN9zmzPDa2d24
|
||||
EAvOt3pYTYBQ1t9KtqImdL0bUqV6fZ6PsWoPCgt+DBuHb+prVPGP7Bkr/uTmznU/
|
||||
+AlTO+12NsYLbr2HHagkXE31DEXE7CSLa8RNjN/UKtz4Ohq7vnowJvG35FCz/mb3
|
||||
FUHbtHTXa2+bGBUOTf/5Hw0CgYBxw0r9EwUhw1qnUYJ5op7OzFAtp+T7m4ul8kCa
|
||||
SCL8TxGsgl+SQ34opE775dtYfoBk9a0RJqVit3D8yg71KFjOTNAIqHJm/Vyyjc+h
|
||||
i9rJDSXiuczsAVfLtPVMRfS0J9QkqeG4PIfkQmVLI/CZ2ZBmsqEcX+eFs4ZfPLun
|
||||
Qsxe2QKBgGuPilIbLeIBDIaPiUI0FwU8v2j8CEQBYvoQn34c95hVQsig/o5z7zlo
|
||||
UsO0wlTngXKlWdOcCs1kqEhTLrstf48djDxAYAxkw40nzeJOt7q52ib/fvf4/UBy
|
||||
X024wzbiw1q07jFCyfQmODzURAx1VNT7QVUMdz/N8vy47/H40AZJ
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`
|
||||
|
||||
func getPrivateKey(data string) *rsa.PrivateKey {
|
||||
key, _ := jwt.ParseRSAPrivateKeyFromPEM([]byte(data))
|
||||
return key
|
||||
}
|
||||
|
||||
func getPublicKey(data string) *rsa.PublicKey {
|
||||
key, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(data))
|
||||
return key
|
||||
}
|
||||
|
||||
func TestReadPrivateKey(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating tmpfile: %v", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
if err := ioutil.WriteFile(f.Name(), []byte(privateKey), os.FileMode(0600)); err != nil {
|
||||
t.Fatalf("error creating tmpfile: %v", err)
|
||||
}
|
||||
|
||||
if _, err := ReadPrivateKey(f.Name()); err != nil {
|
||||
t.Fatalf("error reading key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPublicKey(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating tmpfile: %v", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
if err := ioutil.WriteFile(f.Name(), []byte(publicKey), os.FileMode(0600)); err != nil {
|
||||
t.Fatalf("error creating tmpfile: %v", err)
|
||||
}
|
||||
|
||||
if _, err := ReadPublicKey(f.Name()); err != nil {
|
||||
t.Fatalf("error reading key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenGenerateAndValidate(t *testing.T) {
|
||||
expectedUserName := "serviceaccount:test:my-service-account"
|
||||
expectedUserUID := "12345"
|
||||
|
||||
// Related API objects
|
||||
serviceAccount := &api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "my-service-account",
|
||||
UID: "12345",
|
||||
Namespace: "test",
|
||||
},
|
||||
}
|
||||
secret := &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "my-secret",
|
||||
Namespace: "test",
|
||||
},
|
||||
}
|
||||
|
||||
// Generate the token
|
||||
generator := JWTTokenGenerator(getPrivateKey(privateKey))
|
||||
token, err := generator.GenerateToken(*serviceAccount, *secret)
|
||||
if err != nil {
|
||||
t.Fatalf("error generating token: %v", err)
|
||||
}
|
||||
if len(token) == 0 {
|
||||
t.Fatalf("no token generated")
|
||||
}
|
||||
|
||||
// "Save" the token
|
||||
secret.Data = map[string][]byte{
|
||||
"token": []byte(token),
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
Client client.Interface
|
||||
Keys []*rsa.PublicKey
|
||||
|
||||
ExpectedErr bool
|
||||
ExpectedOK bool
|
||||
ExpectedUserName string
|
||||
ExpectedUserUID string
|
||||
}{
|
||||
"no keys": {
|
||||
Client: nil,
|
||||
Keys: []*rsa.PublicKey{},
|
||||
ExpectedErr: false,
|
||||
ExpectedOK: false,
|
||||
},
|
||||
"invalid keys": {
|
||||
Client: nil,
|
||||
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey)},
|
||||
ExpectedErr: true,
|
||||
ExpectedOK: false,
|
||||
},
|
||||
"valid key": {
|
||||
Client: nil,
|
||||
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
|
||||
ExpectedErr: false,
|
||||
ExpectedOK: true,
|
||||
ExpectedUserName: expectedUserName,
|
||||
ExpectedUserUID: expectedUserUID,
|
||||
},
|
||||
"rotated keys": {
|
||||
Client: nil,
|
||||
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey), getPublicKey(publicKey)},
|
||||
ExpectedErr: false,
|
||||
ExpectedOK: true,
|
||||
ExpectedUserName: expectedUserName,
|
||||
ExpectedUserUID: expectedUserUID,
|
||||
},
|
||||
"valid lookup": {
|
||||
Client: testclient.NewSimpleFake(serviceAccount, secret),
|
||||
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
|
||||
ExpectedErr: false,
|
||||
ExpectedOK: true,
|
||||
ExpectedUserName: expectedUserName,
|
||||
ExpectedUserUID: expectedUserUID,
|
||||
},
|
||||
"invalid secret lookup": {
|
||||
Client: testclient.NewSimpleFake(serviceAccount),
|
||||
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
|
||||
ExpectedErr: true,
|
||||
ExpectedOK: false,
|
||||
},
|
||||
"invalid serviceaccount lookup": {
|
||||
Client: testclient.NewSimpleFake(secret),
|
||||
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
|
||||
ExpectedErr: true,
|
||||
ExpectedOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range testCases {
|
||||
authenticator := JWTTokenAuthenticator(tc.Keys, tc.Client != nil, tc.Client)
|
||||
|
||||
user, ok, err := authenticator.AuthenticateToken(token)
|
||||
if (err != nil) != tc.ExpectedErr {
|
||||
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if ok != tc.ExpectedOK {
|
||||
t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil || !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if user.GetName() != tc.ExpectedUserName {
|
||||
t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, user.GetName())
|
||||
continue
|
||||
}
|
||||
if user.GetUID() != tc.ExpectedUserUID {
|
||||
t.Errorf("%s: Expected userUID=%v, got %v", k, tc.ExpectedUserUID, user.GetUID())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
230
pkg/serviceaccount/serviceaccounts_controller.go
Normal file
230
pkg/serviceaccount/serviceaccounts_controller.go
Normal file
@ -0,0 +1,230 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// nameIndexFunc is an index function that indexes based on an object's name
|
||||
func nameIndexFunc(obj interface{}) (string, error) {
|
||||
meta, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("object has no meta: %v", err)
|
||||
}
|
||||
return meta.Name(), nil
|
||||
}
|
||||
|
||||
type ServiceAccountControllerOptions struct {
|
||||
Name string
|
||||
ServiceAccountResync time.Duration
|
||||
NamespaceResync time.Duration
|
||||
}
|
||||
|
||||
func DefaultServiceAccountControllerOptions() ServiceAccountControllerOptions {
|
||||
return ServiceAccountControllerOptions{Name: "default"}
|
||||
}
|
||||
|
||||
// NewServiceAccountsController returns a new *ServiceAccountsController.
|
||||
func NewServiceAccountsController(cl *client.Client, options ServiceAccountControllerOptions) *ServiceAccountsController {
|
||||
e := &ServiceAccountsController{
|
||||
client: cl,
|
||||
name: options.Name,
|
||||
}
|
||||
|
||||
accountSelector := fields.SelectorFromSet(map[string]string{client.ObjectNameField: options.Name})
|
||||
e.serviceAccounts, e.serviceAccountController = framework.NewIndexerInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func() (runtime.Object, error) {
|
||||
return e.client.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), accountSelector)
|
||||
},
|
||||
WatchFunc: func(rv string) (watch.Interface, error) {
|
||||
return e.client.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), accountSelector, rv)
|
||||
},
|
||||
},
|
||||
&api.ServiceAccount{},
|
||||
options.ServiceAccountResync,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
DeleteFunc: e.serviceAccountDeleted,
|
||||
},
|
||||
cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc},
|
||||
)
|
||||
|
||||
e.namespaces, e.namespaceController = framework.NewIndexerInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func() (runtime.Object, error) {
|
||||
return e.client.Namespaces().List(labels.Everything(), fields.Everything())
|
||||
},
|
||||
WatchFunc: func(rv string) (watch.Interface, error) {
|
||||
return e.client.Namespaces().Watch(labels.Everything(), fields.Everything(), rv)
|
||||
},
|
||||
},
|
||||
&api.Namespace{},
|
||||
options.NamespaceResync,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: e.namespaceAdded,
|
||||
UpdateFunc: e.namespaceUpdated,
|
||||
},
|
||||
cache.Indexers{"name": nameIndexFunc},
|
||||
)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ServiceAccountsController manages ServiceAccount objects inside Namespaces
|
||||
type ServiceAccountsController struct {
|
||||
stopChan chan struct{}
|
||||
|
||||
client *client.Client
|
||||
name string
|
||||
|
||||
serviceAccounts cache.Indexer
|
||||
namespaces cache.Indexer
|
||||
|
||||
// Since we join two objects, we'll watch both of them with controllers.
|
||||
serviceAccountController *framework.Controller
|
||||
namespaceController *framework.Controller
|
||||
}
|
||||
|
||||
// Runs controller loops and returns immediately
|
||||
func (e *ServiceAccountsController) Run() {
|
||||
if e.stopChan == nil {
|
||||
e.stopChan = make(chan struct{})
|
||||
go e.serviceAccountController.Run(e.stopChan)
|
||||
go e.namespaceController.Run(e.stopChan)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down this controller
|
||||
func (e *ServiceAccountsController) Stop() {
|
||||
if e.stopChan != nil {
|
||||
close(e.stopChan)
|
||||
e.stopChan = nil
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountDeleted reacts to a ServiceAccount deletion by recreating a default ServiceAccount in the namespace if needed
|
||||
func (e *ServiceAccountsController) serviceAccountDeleted(obj interface{}) {
|
||||
serviceAccount, ok := obj.(*api.ServiceAccount)
|
||||
if !ok {
|
||||
// Unknown type. If we missed a ServiceAccount deletion, the
|
||||
// corresponding secrets will be cleaned up during the Secret re-list
|
||||
return
|
||||
}
|
||||
e.createDefaultServiceAccountIfNeeded(serviceAccount.Namespace)
|
||||
}
|
||||
|
||||
// namespaceAdded reacts to a Namespace creation by creating a default ServiceAccount object
|
||||
func (e *ServiceAccountsController) namespaceAdded(obj interface{}) {
|
||||
namespace := obj.(*api.Namespace)
|
||||
e.createDefaultServiceAccountIfNeeded(namespace.Name)
|
||||
}
|
||||
|
||||
// namespaceUpdated reacts to a Namespace update (or re-list) by creating a default ServiceAccount in the namespace if needed
|
||||
func (e *ServiceAccountsController) namespaceUpdated(oldObj interface{}, newObj interface{}) {
|
||||
newNamespace := newObj.(*api.Namespace)
|
||||
e.createDefaultServiceAccountIfNeeded(newNamespace.Name)
|
||||
}
|
||||
|
||||
// createDefaultServiceAccountIfNeeded creates a default ServiceAccount in the given namespace if:
|
||||
// * it default ServiceAccount does not already exist
|
||||
// * the specified namespace exists
|
||||
// * the specified namespace is in the ACTIVE phase
|
||||
func (e *ServiceAccountsController) createDefaultServiceAccountIfNeeded(namespace string) {
|
||||
serviceAccount, err := e.getDefaultServiceAccount(namespace)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
if serviceAccount != nil {
|
||||
// If service account already exists, it doesn't need to be created
|
||||
return
|
||||
}
|
||||
|
||||
namespaceObj, err := e.getNamespace(namespace)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
if namespaceObj == nil {
|
||||
// If namespace does not exist, no service account is needed
|
||||
return
|
||||
}
|
||||
if namespaceObj.Status.Phase != api.NamespaceActive {
|
||||
// If namespace is not active, we shouldn't try to create anything
|
||||
return
|
||||
}
|
||||
|
||||
e.createDefaultServiceAccount(namespace)
|
||||
}
|
||||
|
||||
// createDefaultServiceAccount creates a default ServiceAccount in the specified namespace
|
||||
func (e *ServiceAccountsController) createDefaultServiceAccount(namespace string) {
|
||||
serviceAccount := &api.ServiceAccount{}
|
||||
serviceAccount.Name = e.name
|
||||
serviceAccount.Namespace = namespace
|
||||
if _, err := e.client.ServiceAccounts(namespace).Create(serviceAccount); err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// getDefaultServiceAccount returns the default ServiceAccount for the given namespace
|
||||
func (e *ServiceAccountsController) getDefaultServiceAccount(namespace string) (*api.ServiceAccount, error) {
|
||||
|
||||
key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: namespace}}
|
||||
accounts, err := e.serviceAccounts.Index("namespace", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, obj := range accounts {
|
||||
serviceAccount := obj.(*api.ServiceAccount)
|
||||
if e.name == serviceAccount.Name {
|
||||
return serviceAccount, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getNamespace returns the Namespace with the given name
|
||||
func (e *ServiceAccountsController) getNamespace(name string) (*api.Namespace, error) {
|
||||
key := &api.Namespace{ObjectMeta: api.ObjectMeta{Name: name}}
|
||||
namespaces, err := e.namespaces.Index("name", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(namespaces) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(namespaces) == 1 {
|
||||
return namespaces[0].(*api.Namespace), nil
|
||||
}
|
||||
return nil, fmt.Errorf("%d namespaces with the name %s indexed", len(namespaces), name)
|
||||
}
|
165
pkg/serviceaccount/serviceaccounts_controller_test.go
Normal file
165
pkg/serviceaccount/serviceaccounts_controller_test.go
Normal file
@ -0,0 +1,165 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
type serverResponse struct {
|
||||
statusCode int
|
||||
obj interface{}
|
||||
}
|
||||
|
||||
func makeTestServer(t *testing.T, namespace string, serviceAccountResponse serverResponse) (*httptest.Server, *util.FakeHandler) {
|
||||
fakeServiceAccountsHandler := util.FakeHandler{
|
||||
StatusCode: serviceAccountResponse.statusCode,
|
||||
ResponseBody: runtime.EncodeOrDie(testapi.Codec(), serviceAccountResponse.obj.(runtime.Object)),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(testapi.ResourcePath("serviceAccounts", namespace, ""), &fakeServiceAccountsHandler)
|
||||
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
||||
t.Errorf("unexpected request: %v", req.RequestURI)
|
||||
res.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
return httptest.NewServer(mux), &fakeServiceAccountsHandler
|
||||
}
|
||||
|
||||
func TestServiceAccountCreation(t *testing.T) {
|
||||
ns := api.NamespaceDefault
|
||||
|
||||
activeNS := &api.Namespace{
|
||||
ObjectMeta: api.ObjectMeta{Name: ns},
|
||||
Status: api.NamespaceStatus{
|
||||
Phase: api.NamespaceActive,
|
||||
},
|
||||
}
|
||||
terminatingNS := &api.Namespace{
|
||||
ObjectMeta: api.ObjectMeta{Name: ns},
|
||||
Status: api.NamespaceStatus{
|
||||
Phase: api.NamespaceTerminating,
|
||||
},
|
||||
}
|
||||
serviceAccount := &api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: ns,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
}
|
||||
|
||||
testcases := map[string]struct {
|
||||
ExistingNamespace *api.Namespace
|
||||
ExistingServiceAccount *api.ServiceAccount
|
||||
|
||||
AddedNamespace *api.Namespace
|
||||
UpdatedNamespace *api.Namespace
|
||||
DeletedServiceAccount *api.ServiceAccount
|
||||
|
||||
ExpectCreatedServiceAccount bool
|
||||
}{
|
||||
"new active namespace missing serviceaccount": {
|
||||
AddedNamespace: activeNS,
|
||||
ExpectCreatedServiceAccount: true,
|
||||
},
|
||||
"new active namespace with serviceaccount": {
|
||||
ExistingServiceAccount: serviceAccount,
|
||||
AddedNamespace: activeNS,
|
||||
ExpectCreatedServiceAccount: false,
|
||||
},
|
||||
"new terminating namespace": {
|
||||
AddedNamespace: terminatingNS,
|
||||
ExpectCreatedServiceAccount: false,
|
||||
},
|
||||
|
||||
"updated active namespace missing serviceaccount": {
|
||||
UpdatedNamespace: activeNS,
|
||||
ExpectCreatedServiceAccount: true,
|
||||
},
|
||||
"updated active namespace with serviceaccount": {
|
||||
ExistingServiceAccount: serviceAccount,
|
||||
UpdatedNamespace: activeNS,
|
||||
ExpectCreatedServiceAccount: false,
|
||||
},
|
||||
"updated terminating namespace": {
|
||||
UpdatedNamespace: terminatingNS,
|
||||
ExpectCreatedServiceAccount: false,
|
||||
},
|
||||
|
||||
"deleted serviceaccount without namespace": {
|
||||
DeletedServiceAccount: serviceAccount,
|
||||
ExpectCreatedServiceAccount: false,
|
||||
},
|
||||
"deleted serviceaccount with active namespace": {
|
||||
ExistingNamespace: activeNS,
|
||||
DeletedServiceAccount: serviceAccount,
|
||||
ExpectCreatedServiceAccount: true,
|
||||
},
|
||||
"deleted serviceaccount with terminating namespace": {
|
||||
ExistingNamespace: terminatingNS,
|
||||
DeletedServiceAccount: serviceAccount,
|
||||
ExpectCreatedServiceAccount: false,
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range testcases {
|
||||
|
||||
testServer, handler := makeTestServer(t, ns, serverResponse{http.StatusOK, serviceAccount})
|
||||
client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()})
|
||||
controller := NewServiceAccountsController(client, DefaultServiceAccountControllerOptions())
|
||||
|
||||
if tc.ExistingNamespace != nil {
|
||||
controller.namespaces.Add(tc.ExistingNamespace)
|
||||
}
|
||||
if tc.ExistingServiceAccount != nil {
|
||||
controller.serviceAccounts.Add(tc.ExistingServiceAccount)
|
||||
}
|
||||
|
||||
if tc.AddedNamespace != nil {
|
||||
controller.namespaces.Add(tc.AddedNamespace)
|
||||
controller.namespaceAdded(tc.AddedNamespace)
|
||||
}
|
||||
if tc.UpdatedNamespace != nil {
|
||||
controller.namespaces.Add(tc.UpdatedNamespace)
|
||||
controller.namespaceUpdated(nil, tc.UpdatedNamespace)
|
||||
}
|
||||
if tc.DeletedServiceAccount != nil {
|
||||
controller.serviceAccountDeleted(tc.DeletedServiceAccount)
|
||||
}
|
||||
|
||||
if tc.ExpectCreatedServiceAccount {
|
||||
if !handler.ValidateRequestCount(t, 1) {
|
||||
t.Errorf("%s: Expected a single creation call", k)
|
||||
}
|
||||
} else {
|
||||
if !handler.ValidateRequestCount(t, 0) {
|
||||
t.Errorf("%s: Expected no creation calls", k)
|
||||
}
|
||||
}
|
||||
|
||||
testServer.Close()
|
||||
}
|
||||
}
|
442
pkg/serviceaccount/tokens_controller.go
Normal file
442
pkg/serviceaccount/tokens_controller.go
Normal file
@ -0,0 +1,442 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// TokensControllerOptions contains options for the TokensController
|
||||
type TokensControllerOptions struct {
|
||||
// TokenGenerator is the generator to use to create new tokens
|
||||
TokenGenerator TokenGenerator
|
||||
// ServiceAccountResync is the time.Duration at which to fully re-list service accounts.
|
||||
// If zero, re-list will be delayed as long as possible
|
||||
ServiceAccountResync time.Duration
|
||||
// SecretResync is the time.Duration at which to fully re-list secrets.
|
||||
// If zero, re-list will be delayed as long as possible
|
||||
SecretResync time.Duration
|
||||
}
|
||||
|
||||
// DefaultTokenControllerOptions returns
|
||||
func DefaultTokenControllerOptions(tokenGenerator TokenGenerator) TokensControllerOptions {
|
||||
return TokensControllerOptions{TokenGenerator: tokenGenerator}
|
||||
}
|
||||
|
||||
// NewTokensController returns a new *TokensController.
|
||||
func NewTokensController(cl client.Interface, options TokensControllerOptions) *TokensController {
|
||||
e := &TokensController{
|
||||
client: cl,
|
||||
token: options.TokenGenerator,
|
||||
}
|
||||
|
||||
e.serviceAccounts, e.serviceAccountController = framework.NewIndexerInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func() (runtime.Object, error) {
|
||||
return e.client.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), fields.Everything())
|
||||
},
|
||||
WatchFunc: func(rv string) (watch.Interface, error) {
|
||||
return e.client.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), rv)
|
||||
},
|
||||
},
|
||||
&api.ServiceAccount{},
|
||||
options.ServiceAccountResync,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: e.serviceAccountAdded,
|
||||
UpdateFunc: e.serviceAccountUpdated,
|
||||
DeleteFunc: e.serviceAccountDeleted,
|
||||
},
|
||||
cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc},
|
||||
)
|
||||
|
||||
tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
|
||||
e.secrets, e.secretController = framework.NewIndexerInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func() (runtime.Object, error) {
|
||||
return e.client.Secrets(api.NamespaceAll).List(labels.Everything(), tokenSelector)
|
||||
},
|
||||
WatchFunc: func(rv string) (watch.Interface, error) {
|
||||
return e.client.Secrets(api.NamespaceAll).Watch(labels.Everything(), tokenSelector, rv)
|
||||
},
|
||||
},
|
||||
&api.Secret{},
|
||||
options.SecretResync,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: e.secretAdded,
|
||||
UpdateFunc: e.secretUpdated,
|
||||
DeleteFunc: e.secretDeleted,
|
||||
},
|
||||
cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc},
|
||||
)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TokensController manages ServiceAccountToken secrets for ServiceAccount objects
|
||||
type TokensController struct {
|
||||
stopChan chan struct{}
|
||||
|
||||
client client.Interface
|
||||
token TokenGenerator
|
||||
|
||||
serviceAccounts cache.Indexer
|
||||
secrets cache.Indexer
|
||||
|
||||
// Since we join two objects, we'll watch both of them with controllers.
|
||||
serviceAccountController *framework.Controller
|
||||
secretController *framework.Controller
|
||||
}
|
||||
|
||||
// Runs controller loops and returns immediately
|
||||
func (e *TokensController) Run() {
|
||||
if e.stopChan == nil {
|
||||
e.stopChan = make(chan struct{})
|
||||
go e.serviceAccountController.Run(e.stopChan)
|
||||
go e.secretController.Run(e.stopChan)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down this controller
|
||||
func (e *TokensController) Stop() {
|
||||
if e.stopChan != nil {
|
||||
close(e.stopChan)
|
||||
e.stopChan = nil
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountAdded reacts to a ServiceAccount creation by creating a corresponding ServiceAccountToken Secret
|
||||
func (e *TokensController) serviceAccountAdded(obj interface{}) {
|
||||
serviceAccount := obj.(*api.ServiceAccount)
|
||||
err := e.createSecretIfNeeded(serviceAccount)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountUpdated reacts to a ServiceAccount update (or re-list) by ensuring a corresponding ServiceAccountToken Secret exists
|
||||
func (e *TokensController) serviceAccountUpdated(oldObj interface{}, newObj interface{}) {
|
||||
newServiceAccount := newObj.(*api.ServiceAccount)
|
||||
err := e.createSecretIfNeeded(newServiceAccount)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountDeleted reacts to a ServiceAccount deletion by deleting all corresponding ServiceAccountToken Secrets
|
||||
func (e *TokensController) serviceAccountDeleted(obj interface{}) {
|
||||
serviceAccount, ok := obj.(*api.ServiceAccount)
|
||||
if !ok {
|
||||
// Unknown type. If we missed a ServiceAccount deletion, the
|
||||
// corresponding secrets will be cleaned up during the Secret re-list
|
||||
return
|
||||
}
|
||||
secrets, err := e.listTokenSecrets(serviceAccount)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
if err := e.deleteSecret(secret); err != nil {
|
||||
glog.Errorf("Error deleting secret %s/%s: %v", secret.Namespace, secret.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// secretAdded reacts to a Secret create by ensuring the referenced ServiceAccount exists, and by adding a token to the secret if needed
|
||||
func (e *TokensController) secretAdded(obj interface{}) {
|
||||
secret := obj.(*api.Secret)
|
||||
serviceAccount, err := e.getServiceAccount(secret)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
if serviceAccount == nil {
|
||||
if err := e.deleteSecret(secret); err != nil {
|
||||
glog.Errorf("Error deleting secret %s/%s: %v", secret.Namespace, secret.Name, err)
|
||||
}
|
||||
} else {
|
||||
e.generateTokenIfNeeded(serviceAccount, secret)
|
||||
}
|
||||
}
|
||||
|
||||
// secretUpdated reacts to a Secret update (or re-list) by deleting the secret (if the referenced ServiceAccount does not exist)
|
||||
func (e *TokensController) secretUpdated(oldObj interface{}, newObj interface{}) {
|
||||
newSecret := newObj.(*api.Secret)
|
||||
newServiceAccount, err := e.getServiceAccount(newSecret)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
if newServiceAccount == nil {
|
||||
if err := e.deleteSecret(newSecret); err != nil {
|
||||
glog.Errorf("Error deleting secret %s/%s: %v", newSecret.Namespace, newSecret.Name, err)
|
||||
}
|
||||
} else {
|
||||
e.generateTokenIfNeeded(newServiceAccount, newSecret)
|
||||
}
|
||||
}
|
||||
|
||||
// secretDeleted reacts to a Secret being deleted by removing a reference from the corresponding ServiceAccount if needed
|
||||
func (e *TokensController) secretDeleted(obj interface{}) {
|
||||
secret, ok := obj.(*api.Secret)
|
||||
if !ok {
|
||||
// Unknown type. If we missed a Secret deletion, the corresponding ServiceAccount (if it exists)
|
||||
// will get a secret recreated (if needed) during the ServiceAccount re-list
|
||||
return
|
||||
}
|
||||
|
||||
serviceAccount, err := e.getServiceAccount(secret)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
if serviceAccount == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := e.removeSecretReferenceIfNeeded(serviceAccount, secret.Name); err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// createSecretIfNeeded makes sure at least one ServiceAccountToken secret exists, and is included in the serviceAccount's Secrets list
|
||||
func (e *TokensController) createSecretIfNeeded(serviceAccount *api.ServiceAccount) error {
|
||||
// If the service account references no secrets, short-circuit and create a new one
|
||||
if len(serviceAccount.Secrets) == 0 {
|
||||
return e.createSecret(serviceAccount)
|
||||
}
|
||||
|
||||
// If any existing token secrets are referenced by the service account, return
|
||||
allSecrets, err := e.listTokenSecrets(serviceAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
referencedSecrets := getSecretReferences(serviceAccount)
|
||||
for _, secret := range allSecrets {
|
||||
if referencedSecrets.Has(secret.Name) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise create a new token secret
|
||||
return e.createSecret(serviceAccount)
|
||||
}
|
||||
|
||||
// createSecret creates a secret of type ServiceAccountToken for the given ServiceAccount
|
||||
func (e *TokensController) createSecret(serviceAccount *api.ServiceAccount) error {
|
||||
// Build the secret
|
||||
secret := &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: secret.Strategy.GenerateName(fmt.Sprintf("%s-token-", serviceAccount.Name)),
|
||||
Namespace: serviceAccount.Namespace,
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: serviceAccount.Name,
|
||||
api.ServiceAccountUIDKey: string(serviceAccount.UID),
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{},
|
||||
}
|
||||
|
||||
// Generate the token
|
||||
token, err := e.token.GenerateToken(*serviceAccount, *secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.Data[api.ServiceAccountTokenKey] = []byte(token)
|
||||
|
||||
// Save the secret
|
||||
if _, err := e.client.Secrets(serviceAccount.Namespace).Create(secret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We don't want to update the cache's copy of the service account
|
||||
// so add the secret to a freshly retrieved copy of the service account
|
||||
serviceAccounts := e.client.ServiceAccounts(serviceAccount.Namespace)
|
||||
serviceAccount, err = serviceAccounts.Get(serviceAccount.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceAccount.Secrets = append(serviceAccount.Secrets, api.ObjectReference{Name: secret.Name})
|
||||
|
||||
_, err = serviceAccounts.Update(serviceAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateTokenIfNeeded populates the token data for the given Secret if not already set
|
||||
func (e *TokensController) generateTokenIfNeeded(serviceAccount *api.ServiceAccount, secret *api.Secret) error {
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = map[string]string{}
|
||||
}
|
||||
if secret.Data == nil {
|
||||
secret.Data = map[string][]byte{}
|
||||
}
|
||||
|
||||
tokenData, ok := secret.Data[api.ServiceAccountTokenKey]
|
||||
if ok && len(tokenData) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate the token
|
||||
token, err := e.token.GenerateToken(*serviceAccount, *secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the token and annotations
|
||||
secret.Data[api.ServiceAccountTokenKey] = []byte(token)
|
||||
secret.Annotations[api.ServiceAccountNameKey] = serviceAccount.Name
|
||||
secret.Annotations[api.ServiceAccountUIDKey] = string(serviceAccount.UID)
|
||||
|
||||
// Save the secret
|
||||
if _, err := e.client.Secrets(secret.Namespace).Update(secret); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteSecret deletes the given secret
|
||||
func (e *TokensController) deleteSecret(secret *api.Secret) error {
|
||||
return e.client.Secrets(secret.Namespace).Delete(secret.Name)
|
||||
}
|
||||
|
||||
// removeSecretReferenceIfNeeded updates the given ServiceAccount to remove a reference to the given secretName if needed.
|
||||
// Returns whether an update was performed, and any error that occurred
|
||||
func (e *TokensController) removeSecretReferenceIfNeeded(serviceAccount *api.ServiceAccount, secretName string) (bool, error) {
|
||||
// See if the account even referenced the secret
|
||||
if !getSecretReferences(serviceAccount).Has(secretName) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// We don't want to update the cache's copy of the service account
|
||||
// so remove the secret from a freshly retrieved copy of the service account
|
||||
serviceAccounts := e.client.ServiceAccounts(serviceAccount.Namespace)
|
||||
serviceAccount, err := serviceAccounts.Get(serviceAccount.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Double-check to see if the account still references the secret
|
||||
if !getSecretReferences(serviceAccount).Has(secretName) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
secrets := []api.ObjectReference{}
|
||||
for _, s := range serviceAccount.Secrets {
|
||||
if s.Name != secretName {
|
||||
secrets = append(secrets, s)
|
||||
}
|
||||
}
|
||||
serviceAccount.Secrets = secrets
|
||||
|
||||
_, err = serviceAccounts.Update(serviceAccount)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getServiceAccount returns the ServiceAccount referenced by the given secret. If the secret is not
|
||||
// of type ServiceAccountToken, or if the referenced ServiceAccount does not exist, nil is returned
|
||||
func (e *TokensController) getServiceAccount(secret *api.Secret) (*api.ServiceAccount, error) {
|
||||
name, uid := serviceAccountNameAndUID(secret)
|
||||
if len(name) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: secret.Namespace}}
|
||||
namespaceAccounts, err := e.serviceAccounts.Index("namespace", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, obj := range namespaceAccounts {
|
||||
serviceAccount := obj.(*api.ServiceAccount)
|
||||
if name != serviceAccount.Name {
|
||||
// Name must match
|
||||
continue
|
||||
}
|
||||
if len(uid) > 0 && uid != string(serviceAccount.UID) {
|
||||
// If UID is specified, it must match
|
||||
continue
|
||||
}
|
||||
return serviceAccount, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// listTokenSecrets returns a list of all of the ServiceAccountToken secrets that
|
||||
// reference the given service account's name and uid
|
||||
func (e *TokensController) listTokenSecrets(serviceAccount *api.ServiceAccount) ([]*api.Secret, error) {
|
||||
key := &api.Secret{ObjectMeta: api.ObjectMeta{Namespace: serviceAccount.Namespace}}
|
||||
namespaceSecrets, err := e.secrets.Index("namespace", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := []*api.Secret{}
|
||||
for _, obj := range namespaceSecrets {
|
||||
secret := obj.(*api.Secret)
|
||||
name, uid := serviceAccountNameAndUID(secret)
|
||||
if name != serviceAccount.Name {
|
||||
// Name must match
|
||||
continue
|
||||
}
|
||||
if len(uid) > 0 && uid != string(serviceAccount.UID) {
|
||||
// If UID is specified, it must match
|
||||
continue
|
||||
}
|
||||
items = append(items, secret)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// serviceAccountNameAndUID is a helper method to get the ServiceAccount Name and UID from the given secret
|
||||
// Returns "","" if the secret is not a ServiceAccountToken secret
|
||||
// If the name or uid annotation is missing, "" is returned instead
|
||||
func serviceAccountNameAndUID(secret *api.Secret) (string, string) {
|
||||
if secret.Type != api.SecretTypeServiceAccountToken {
|
||||
return "", ""
|
||||
}
|
||||
return secret.Annotations[api.ServiceAccountNameKey], secret.Annotations[api.ServiceAccountUIDKey]
|
||||
}
|
||||
|
||||
func getSecretReferences(serviceAccount *api.ServiceAccount) util.StringSet {
|
||||
references := util.NewStringSet()
|
||||
for _, secret := range serviceAccount.Secrets {
|
||||
references.Insert(secret.Name)
|
||||
}
|
||||
return references
|
||||
}
|
385
pkg/serviceaccount/tokens_controller_test.go
Normal file
385
pkg/serviceaccount/tokens_controller_test.go
Normal file
@ -0,0 +1,385 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
)
|
||||
|
||||
type testGenerator struct {
|
||||
GeneratedServiceAccounts []api.ServiceAccount
|
||||
GeneratedSecrets []api.Secret
|
||||
Token string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (t *testGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
|
||||
t.GeneratedSecrets = append(t.GeneratedSecrets, secret)
|
||||
t.GeneratedServiceAccounts = append(t.GeneratedServiceAccounts, serviceAccount)
|
||||
return t.Token, t.Err
|
||||
}
|
||||
|
||||
// emptySecretReferences is used by a service account without any secrets
|
||||
func emptySecretReferences() []api.ObjectReference {
|
||||
return []api.ObjectReference{}
|
||||
}
|
||||
|
||||
// missingSecretReferences is used by a service account that references secrets which do no exist
|
||||
func missingSecretReferences() []api.ObjectReference {
|
||||
return []api.ObjectReference{{Name: "missing-secret-1"}}
|
||||
}
|
||||
|
||||
// regularSecretReferences is used by a service account that references secrets which are not ServiceAccountTokens
|
||||
func regularSecretReferences() []api.ObjectReference {
|
||||
return []api.ObjectReference{{Name: "regular-secret-1"}}
|
||||
}
|
||||
|
||||
// tokenSecretReferences is used by a service account that references a ServiceAccountToken secret
|
||||
func tokenSecretReferences() []api.ObjectReference {
|
||||
return []api.ObjectReference{{Name: "token-secret-1"}}
|
||||
}
|
||||
|
||||
// addTokenSecretReference adds a reference to the ServiceAccountToken that will be created
|
||||
func addTokenSecretReference(refs []api.ObjectReference) []api.ObjectReference {
|
||||
return append(refs, api.ObjectReference{Name: "default-token-fplln"})
|
||||
}
|
||||
|
||||
// serviceAccount returns a service account with the given secret refs
|
||||
func serviceAccount(secretRefs []api.ObjectReference) *api.ServiceAccount {
|
||||
return &api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "default",
|
||||
UID: "12345",
|
||||
Namespace: "default",
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Secrets: secretRefs,
|
||||
}
|
||||
}
|
||||
|
||||
// opaqueSecret returns a persisted non-ServiceAccountToken secret named "regular-secret-1"
|
||||
func opaqueSecret() *api.Secret {
|
||||
return &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "regular-secret-1",
|
||||
Namespace: "default",
|
||||
UID: "23456",
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Type: "Opaque",
|
||||
Data: map[string][]byte{
|
||||
"mykey": []byte("mydata"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createdTokenSecret returns the ServiceAccountToken secret posted when creating a new token secret.
|
||||
// Named "default-token-fplln", since that is the first generated name after rand.Seed(1)
|
||||
func createdTokenSecret() *api.Secret {
|
||||
return &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "default-token-fplln",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: "default",
|
||||
api.ServiceAccountUIDKey: "12345",
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{
|
||||
"token": []byte("ABC"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountTokenSecret returns an existing ServiceAccountToken secret named "token-secret-1"
|
||||
func serviceAccountTokenSecret() *api.Secret {
|
||||
return &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "token-secret-1",
|
||||
Namespace: "default",
|
||||
UID: "23456",
|
||||
ResourceVersion: "1",
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: "default",
|
||||
api.ServiceAccountUIDKey: "12345",
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{
|
||||
"token": []byte("ABC"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountTokenSecretWithoutTokenData returns an existing ServiceAccountToken secret that lacks token data
|
||||
func serviceAccountTokenSecretWithoutTokenData() *api.Secret {
|
||||
secret := serviceAccountTokenSecret()
|
||||
secret.Data = nil
|
||||
return secret
|
||||
}
|
||||
|
||||
func TestTokenCreation(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
ClientObjects []runtime.Object
|
||||
|
||||
ExistingServiceAccount *api.ServiceAccount
|
||||
ExistingSecrets []*api.Secret
|
||||
|
||||
AddedServiceAccount *api.ServiceAccount
|
||||
UpdatedServiceAccount *api.ServiceAccount
|
||||
DeletedServiceAccount *api.ServiceAccount
|
||||
AddedSecret *api.Secret
|
||||
UpdatedSecret *api.Secret
|
||||
DeletedSecret *api.Secret
|
||||
|
||||
ExpectedActions []testclient.FakeAction
|
||||
}{
|
||||
"new serviceaccount with no secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()},
|
||||
|
||||
AddedServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(emptySecretReferences()))},
|
||||
},
|
||||
},
|
||||
"new serviceaccount with missing secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences()), createdTokenSecret()},
|
||||
|
||||
AddedServiceAccount: serviceAccount(missingSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(missingSecretReferences()))},
|
||||
},
|
||||
},
|
||||
"new serviceaccount with non-token secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), createdTokenSecret(), opaqueSecret()},
|
||||
|
||||
AddedServiceAccount: serviceAccount(regularSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(regularSecretReferences()))},
|
||||
},
|
||||
},
|
||||
"new serviceaccount with token secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences()), serviceAccountTokenSecret()},
|
||||
ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()},
|
||||
|
||||
AddedServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
|
||||
"updated serviceaccount with no secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()},
|
||||
|
||||
UpdatedServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(emptySecretReferences()))},
|
||||
},
|
||||
},
|
||||
"updated serviceaccount with missing secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences()), createdTokenSecret()},
|
||||
|
||||
UpdatedServiceAccount: serviceAccount(missingSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(missingSecretReferences()))},
|
||||
},
|
||||
},
|
||||
"updated serviceaccount with non-token secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), createdTokenSecret(), opaqueSecret()},
|
||||
|
||||
UpdatedServiceAccount: serviceAccount(regularSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(regularSecretReferences()))},
|
||||
},
|
||||
},
|
||||
"updated serviceaccount with token secrets": {
|
||||
ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()},
|
||||
|
||||
UpdatedServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
|
||||
"deleted serviceaccount with no secrets": {
|
||||
DeletedServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"deleted serviceaccount with missing secrets": {
|
||||
DeletedServiceAccount: serviceAccount(missingSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"deleted serviceaccount with non-token secrets": {
|
||||
ClientObjects: []runtime.Object{opaqueSecret()},
|
||||
|
||||
DeletedServiceAccount: serviceAccount(regularSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"deleted serviceaccount with token secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
|
||||
ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()},
|
||||
|
||||
DeletedServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "delete-secret", Value: "token-secret-1"},
|
||||
},
|
||||
},
|
||||
|
||||
"added secret without serviceaccount": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
|
||||
|
||||
AddedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "delete-secret", Value: "token-secret-1"},
|
||||
},
|
||||
},
|
||||
"added secret with serviceaccount": {
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
AddedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"added token secret without token data": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()},
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
AddedSecret: serviceAccountTokenSecretWithoutTokenData(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "update-secret", Value: serviceAccountTokenSecret()},
|
||||
},
|
||||
},
|
||||
|
||||
"updated secret without serviceaccount": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
|
||||
|
||||
UpdatedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "delete-secret", Value: "token-secret-1"},
|
||||
},
|
||||
},
|
||||
"updated secret with serviceaccount": {
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
UpdatedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"updated token secret without token data": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()},
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
UpdatedSecret: serviceAccountTokenSecretWithoutTokenData(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "update-secret", Value: serviceAccountTokenSecret()},
|
||||
},
|
||||
},
|
||||
|
||||
"deleted secret without serviceaccount": {
|
||||
DeletedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"deleted secret with serviceaccount with reference": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences())},
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
DeletedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(emptySecretReferences())},
|
||||
},
|
||||
},
|
||||
"deleted secret with serviceaccount without reference": {
|
||||
ExistingServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
|
||||
DeletedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range testcases {
|
||||
|
||||
// Re-seed to reset name generation
|
||||
rand.Seed(1)
|
||||
|
||||
generator := &testGenerator{Token: "ABC"}
|
||||
|
||||
client := testclient.NewSimpleFake(tc.ClientObjects...)
|
||||
|
||||
controller := NewTokensController(client, DefaultTokenControllerOptions(generator))
|
||||
|
||||
if tc.ExistingServiceAccount != nil {
|
||||
controller.serviceAccounts.Add(tc.ExistingServiceAccount)
|
||||
}
|
||||
for _, s := range tc.ExistingSecrets {
|
||||
controller.secrets.Add(s)
|
||||
}
|
||||
|
||||
if tc.AddedServiceAccount != nil {
|
||||
controller.serviceAccountAdded(tc.AddedServiceAccount)
|
||||
}
|
||||
if tc.UpdatedServiceAccount != nil {
|
||||
controller.serviceAccountUpdated(nil, tc.UpdatedServiceAccount)
|
||||
}
|
||||
if tc.DeletedServiceAccount != nil {
|
||||
controller.serviceAccountDeleted(tc.DeletedServiceAccount)
|
||||
}
|
||||
if tc.AddedSecret != nil {
|
||||
controller.secretAdded(tc.AddedSecret)
|
||||
}
|
||||
if tc.UpdatedSecret != nil {
|
||||
controller.secretUpdated(nil, tc.UpdatedSecret)
|
||||
}
|
||||
if tc.DeletedSecret != nil {
|
||||
controller.secretDeleted(tc.DeletedSecret)
|
||||
}
|
||||
|
||||
for i, action := range client.Actions {
|
||||
if len(tc.ExpectedActions) < i+1 {
|
||||
t.Errorf("%s: %d unexpected actions: %+v", k, len(client.Actions)-len(tc.ExpectedActions), client.Actions[i:])
|
||||
break
|
||||
}
|
||||
|
||||
expectedAction := tc.ExpectedActions[i]
|
||||
if expectedAction.Action != action.Action {
|
||||
t.Errorf("%s: Expected %s, got %s", k, expectedAction.Action, action.Action)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(expectedAction.Value, action.Value) {
|
||||
t.Errorf("%s: Expected\n\t%#v\ngot\n\t%#v", k, expectedAction.Value, action.Value)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(tc.ExpectedActions) > len(client.Actions) {
|
||||
t.Errorf("%s: %d additional expected actions:%+v", k, len(tc.ExpectedActions)-len(client.Actions), tc.ExpectedActions[len(client.Actions):])
|
||||
}
|
||||
}
|
||||
}
|
@ -73,13 +73,16 @@ func (f *FakeHandler) ServeHTTP(response http.ResponseWriter, request *http.Requ
|
||||
f.RequestBody = string(bodyReceived)
|
||||
}
|
||||
|
||||
func (f *FakeHandler) ValidateRequestCount(t TestInterface, count int) {
|
||||
func (f *FakeHandler) ValidateRequestCount(t TestInterface, count int) bool {
|
||||
ok := true
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
if f.requestCount != count {
|
||||
ok = false
|
||||
t.Logf("Expected %d call, but got %d. Only the last call is recorded and checked.", count, f.requestCount)
|
||||
}
|
||||
f.hasBeenChecked = true
|
||||
return ok
|
||||
}
|
||||
|
||||
// ValidateRequest verifies that FakeHandler received a request with expected path, method, and body.
|
||||
|
373
plugin/pkg/admission/serviceaccount/admission.go
Normal file
373
plugin/pkg/admission/serviceaccount/admission.go
Normal file
@ -0,0 +1,373 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
// DefaultServiceAccountName is the name of the default service account to set on pods which do not specify a service account
|
||||
const DefaultServiceAccountName = "default"
|
||||
|
||||
// DefaultAPITokenMountPath is the path that ServiceAccountToken secrets are automounted to.
|
||||
// The token file would then be accessible at /var/run/secrets/kubernetes.io/serviceaccount
|
||||
const DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
|
||||
|
||||
func init() {
|
||||
admission.RegisterPlugin("ServiceAccount", func(client client.Interface, config io.Reader) (admission.Interface, error) {
|
||||
serviceAccountAdmission := NewServiceAccount(client)
|
||||
serviceAccountAdmission.Run()
|
||||
return serviceAccountAdmission, nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = admission.Interface(&serviceAccount{})
|
||||
|
||||
type serviceAccount struct {
|
||||
// LimitSecretReferences rejects pods that reference secrets their service accounts do not reference
|
||||
LimitSecretReferences bool
|
||||
// MountServiceAccountToken creates Volume and VolumeMounts for the first referenced ServiceAccountToken for the pod's service account
|
||||
MountServiceAccountToken bool
|
||||
|
||||
client client.Interface
|
||||
|
||||
serviceAccounts cache.Indexer
|
||||
secrets cache.Indexer
|
||||
|
||||
stopChan chan struct{}
|
||||
serviceAccountsReflector *cache.Reflector
|
||||
secretsReflector *cache.Reflector
|
||||
}
|
||||
|
||||
// NewServiceAccount returns an admission.Interface implementation which limits admission of Pod CREATE requests based on the pod's ServiceAccount:
|
||||
// 1. If the pod does not specify a ServiceAccount, it sets the pod's ServiceAccount to "default"
|
||||
// 2. It ensures the ServiceAccount referenced by the pod exists
|
||||
// 3. If LimitSecretReferences is true, it rejects the pod if the pod references Secret objects which the pod's ServiceAccount does not reference
|
||||
// 4. If MountServiceAccountToken is true, it adds a VolumeMount with the pod's ServiceAccount's api token secret to containers
|
||||
func NewServiceAccount(cl client.Interface) *serviceAccount {
|
||||
serviceAccountsIndexer, serviceAccountsReflector := cache.NewNamespaceKeyedIndexerAndReflector(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func() (runtime.Object, error) {
|
||||
return cl.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), fields.Everything())
|
||||
},
|
||||
WatchFunc: func(resourceVersion string) (watch.Interface, error) {
|
||||
return cl.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), resourceVersion)
|
||||
},
|
||||
},
|
||||
&api.ServiceAccount{},
|
||||
0,
|
||||
)
|
||||
|
||||
tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
|
||||
secretsIndexer, secretsReflector := cache.NewNamespaceKeyedIndexerAndReflector(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func() (runtime.Object, error) {
|
||||
return cl.Secrets(api.NamespaceAll).List(labels.Everything(), tokenSelector)
|
||||
},
|
||||
WatchFunc: func(resourceVersion string) (watch.Interface, error) {
|
||||
return cl.Secrets(api.NamespaceAll).Watch(labels.Everything(), tokenSelector, resourceVersion)
|
||||
},
|
||||
},
|
||||
&api.Secret{},
|
||||
0,
|
||||
)
|
||||
|
||||
return &serviceAccount{
|
||||
// TODO: enable this once we've swept secret usage to account for adding secret references to service accounts
|
||||
LimitSecretReferences: false,
|
||||
// Auto mount service account API token secrets
|
||||
MountServiceAccountToken: true,
|
||||
|
||||
client: cl,
|
||||
serviceAccounts: serviceAccountsIndexer,
|
||||
serviceAccountsReflector: serviceAccountsReflector,
|
||||
secrets: secretsIndexer,
|
||||
secretsReflector: secretsReflector,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceAccount) Run() {
|
||||
if s.stopChan == nil {
|
||||
s.stopChan = make(chan struct{})
|
||||
s.serviceAccountsReflector.RunUntil(s.stopChan)
|
||||
s.secretsReflector.RunUntil(s.stopChan)
|
||||
}
|
||||
}
|
||||
func (s *serviceAccount) Stop() {
|
||||
if s.stopChan != nil {
|
||||
close(s.stopChan)
|
||||
s.stopChan = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceAccount) Admit(a admission.Attributes) (err error) {
|
||||
// We only care about Pod CREATE operations
|
||||
if a.GetOperation() != "CREATE" {
|
||||
return nil
|
||||
}
|
||||
if a.GetResource() != string(api.ResourcePods) {
|
||||
return nil
|
||||
}
|
||||
obj := a.GetObject()
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
pod, ok := obj.(*api.Pod)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't modify the spec of mirror pods.
|
||||
// That makes the kubelet very angry and confused, and it immediately deletes the pod (because the spec doesn't match)
|
||||
// That said, don't allow mirror pods to reference ServiceAccounts or SecretVolumeSources either
|
||||
if _, isMirrorPod := pod.Annotations[kubelet.ConfigMirrorAnnotationKey]; isMirrorPod {
|
||||
if len(pod.Spec.ServiceAccount) != 0 {
|
||||
return admission.NewForbidden(a, fmt.Errorf("A mirror pod may not reference service accounts"))
|
||||
}
|
||||
for _, volume := range pod.Spec.Volumes {
|
||||
if volume.VolumeSource.Secret != nil {
|
||||
return admission.NewForbidden(a, fmt.Errorf("A mirror pod may not reference secrets"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set the default service account if needed
|
||||
if len(pod.Spec.ServiceAccount) == 0 {
|
||||
pod.Spec.ServiceAccount = DefaultServiceAccountName
|
||||
}
|
||||
|
||||
// Ensure the referenced service account exists
|
||||
serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccount)
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, fmt.Errorf("Error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccount, err))
|
||||
}
|
||||
if serviceAccount == nil {
|
||||
return admission.NewForbidden(a, fmt.Errorf("Missing service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccount, err))
|
||||
}
|
||||
|
||||
if s.LimitSecretReferences {
|
||||
if err := s.limitSecretReferences(serviceAccount, pod); err != nil {
|
||||
return admission.NewForbidden(a, err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.MountServiceAccountToken {
|
||||
if err := s.mountServiceAccountToken(serviceAccount, pod); err != nil {
|
||||
return admission.NewForbidden(a, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getServiceAccount returns the ServiceAccount for the given namespace and name if it exists
|
||||
func (s *serviceAccount) getServiceAccount(namespace string, name string) (*api.ServiceAccount, error) {
|
||||
key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: namespace}}
|
||||
index, err := s.serviceAccounts.Index("namespace", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, obj := range index {
|
||||
serviceAccount := obj.(*api.ServiceAccount)
|
||||
if serviceAccount.Name == name {
|
||||
return serviceAccount, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Could not find in cache, attempt to look up directly
|
||||
numAttempts := 1
|
||||
if name == DefaultServiceAccountName {
|
||||
// If this is the default serviceaccount, attempt more times, since it should be auto-created by the controller
|
||||
numAttempts = 10
|
||||
}
|
||||
retryInterval := time.Duration(rand.Int63n(100)+int64(100)) * time.Millisecond
|
||||
for i := 0; i < numAttempts; i++ {
|
||||
if i != 0 {
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
serviceAccount, err := s.client.ServiceAccounts(namespace).Get(name)
|
||||
if err == nil {
|
||||
return serviceAccount, nil
|
||||
}
|
||||
if !errors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getReferencedServiceAccountToken returns the name of the first referenced secret which is a ServiceAccountToken for the service account
|
||||
func (s *serviceAccount) getReferencedServiceAccountToken(serviceAccount *api.ServiceAccount) (string, error) {
|
||||
if len(serviceAccount.Secrets) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
tokens, err := s.getServiceAccountTokens(serviceAccount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
references := util.NewStringSet()
|
||||
for _, secret := range serviceAccount.Secrets {
|
||||
references.Insert(secret.Name)
|
||||
}
|
||||
for _, token := range tokens {
|
||||
if references.Has(token.Name) {
|
||||
return token.Name, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// getServiceAccountTokens returns all ServiceAccountToken secrets for the given ServiceAccount
|
||||
func (s *serviceAccount) getServiceAccountTokens(serviceAccount *api.ServiceAccount) ([]*api.Secret, error) {
|
||||
key := &api.Secret{ObjectMeta: api.ObjectMeta{Namespace: serviceAccount.Namespace}}
|
||||
index, err := s.secrets.Index("namespace", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokens := []*api.Secret{}
|
||||
for _, obj := range index {
|
||||
token := obj.(*api.Secret)
|
||||
if token.Type != api.SecretTypeServiceAccountToken {
|
||||
continue
|
||||
}
|
||||
name := token.Annotations[api.ServiceAccountNameKey]
|
||||
uid := token.Annotations[api.ServiceAccountUIDKey]
|
||||
if name != serviceAccount.Name {
|
||||
// Name must match
|
||||
continue
|
||||
}
|
||||
if len(uid) > 0 && uid != string(serviceAccount.UID) {
|
||||
// If UID is set, it must match
|
||||
continue
|
||||
}
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func (s *serviceAccount) limitSecretReferences(serviceAccount *api.ServiceAccount, pod *api.Pod) error {
|
||||
// Ensure all secrets the pod references are allowed by the service account
|
||||
referencedSecrets := util.NewStringSet()
|
||||
for _, s := range serviceAccount.Secrets {
|
||||
referencedSecrets.Insert(s.Name)
|
||||
}
|
||||
for _, volume := range pod.Spec.Volumes {
|
||||
source := volume.VolumeSource
|
||||
if source.Secret == nil {
|
||||
continue
|
||||
}
|
||||
secretName := source.Secret.SecretName
|
||||
if !referencedSecrets.Has(secretName) {
|
||||
return fmt.Errorf("Volume with secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", secretName, serviceAccount.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *serviceAccount) mountServiceAccountToken(serviceAccount *api.ServiceAccount, pod *api.Pod) error {
|
||||
// Find the name of a referenced ServiceAccountToken secret we can mount
|
||||
serviceAccountToken, err := s.getReferencedServiceAccountToken(serviceAccount)
|
||||
if err != nil {
|
||||
fmt.Errorf("Error looking up service account token for %s/%s: %v", serviceAccount.Namespace, serviceAccount.Name, err)
|
||||
}
|
||||
if len(serviceAccountToken) == 0 {
|
||||
// We don't have an API token to mount, so return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the volume and volume name for the ServiceAccountTokenSecret if it already exists
|
||||
tokenVolumeName := ""
|
||||
hasTokenVolume := false
|
||||
allVolumeNames := util.NewStringSet()
|
||||
for _, volume := range pod.Spec.Volumes {
|
||||
allVolumeNames.Insert(volume.Name)
|
||||
if volume.Secret != nil && volume.Secret.SecretName == serviceAccountToken {
|
||||
tokenVolumeName = volume.Name
|
||||
hasTokenVolume = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Determine a volume name for the ServiceAccountTokenSecret in case we need it
|
||||
if len(tokenVolumeName) == 0 {
|
||||
// Try naming the volume the same as the serviceAccountToken, and uniquify if needed
|
||||
tokenVolumeName = serviceAccountToken
|
||||
if allVolumeNames.Has(tokenVolumeName) {
|
||||
tokenVolumeName = api.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", serviceAccountToken))
|
||||
}
|
||||
}
|
||||
|
||||
// Create the prototypical VolumeMount
|
||||
volumeMount := api.VolumeMount{
|
||||
Name: tokenVolumeName,
|
||||
ReadOnly: true,
|
||||
MountPath: DefaultAPITokenMountPath,
|
||||
}
|
||||
|
||||
// Ensure every container mounts the APISecret volume
|
||||
needsTokenVolume := false
|
||||
for i, container := range pod.Spec.Containers {
|
||||
existingContainerMount := false
|
||||
for _, volumeMount := range container.VolumeMounts {
|
||||
// Existing mounts at the default mount path prevent mounting of the API token
|
||||
if volumeMount.MountPath == DefaultAPITokenMountPath {
|
||||
existingContainerMount = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !existingContainerMount {
|
||||
pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, volumeMount)
|
||||
needsTokenVolume = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add the volume if a container needs it
|
||||
if !hasTokenVolume && needsTokenVolume {
|
||||
volume := api.Volume{
|
||||
Name: tokenVolumeName,
|
||||
VolumeSource: api.VolumeSource{
|
||||
Secret: &api.SecretVolumeSource{
|
||||
SecretName: serviceAccountToken,
|
||||
},
|
||||
},
|
||||
}
|
||||
pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
|
||||
}
|
||||
return nil
|
||||
}
|
399
plugin/pkg/admission/serviceaccount/admission_test.go
Normal file
399
plugin/pkg/admission/serviceaccount/admission_test.go
Normal file
@ -0,0 +1,399 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
|
||||
)
|
||||
|
||||
func TestIgnoresNonCreate(t *testing.T) {
|
||||
pod := &api.Pod{}
|
||||
for _, op := range []string{"UPDATE", "DELETE", "CUSTOM"} {
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), op)
|
||||
err := NewServiceAccount(nil).Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %s operation allowed, got err: %v", op, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoresNonPodResource(t *testing.T) {
|
||||
pod := &api.Pod{}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", "myns", "CustomResource", "CREATE")
|
||||
err := NewServiceAccount(nil).Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Expected non-pod resource allowed, got err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoresNilObject(t *testing.T) {
|
||||
attrs := admission.NewAttributesRecord(nil, "Pod", "myns", string(api.ResourcePods), "CREATE")
|
||||
err := NewServiceAccount(nil).Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil object allowed allowed, got err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoresNonPodObject(t *testing.T) {
|
||||
obj := &api.Namespace{}
|
||||
attrs := admission.NewAttributesRecord(obj, "Pod", "myns", string(api.ResourcePods), "CREATE")
|
||||
err := NewServiceAccount(nil).Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Expected non pod object allowed, got err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoresMirrorPod(t *testing.T) {
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
kubelet.ConfigMirrorAnnotationKey: "true",
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Volumes: []api.Volume{
|
||||
{VolumeSource: api.VolumeSource{}},
|
||||
},
|
||||
},
|
||||
}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE")
|
||||
err := NewServiceAccount(nil).Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Expected mirror pod without service account or secrets allowed, got err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsMirrorPodWithServiceAccount(t *testing.T) {
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
kubelet.ConfigMirrorAnnotationKey: "true",
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
ServiceAccount: "default",
|
||||
},
|
||||
}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE")
|
||||
err := NewServiceAccount(nil).Admit(attrs)
|
||||
if err == nil {
|
||||
t.Errorf("Expected a mirror pod to be prevented from referencing a service account")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsMirrorPodWithSecretVolumes(t *testing.T) {
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
kubelet.ConfigMirrorAnnotationKey: "true",
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Volumes: []api.Volume{
|
||||
{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE")
|
||||
err := NewServiceAccount(nil).Admit(attrs)
|
||||
if err == nil {
|
||||
t.Errorf("Expected a mirror pod to be prevented from referencing a secret volume")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignsDefaultServiceAccountAndToleratesMissingAPIToken(t *testing.T) {
|
||||
ns := "myns"
|
||||
|
||||
admit := NewServiceAccount(nil)
|
||||
admit.MountServiceAccountToken = true
|
||||
|
||||
// Add the default service account for the ns into the cache
|
||||
admit.serviceAccounts.Add(&api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: DefaultServiceAccountName,
|
||||
Namespace: ns,
|
||||
},
|
||||
})
|
||||
|
||||
pod := &api.Pod{}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
|
||||
err := admit.Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if pod.Spec.ServiceAccount != DefaultServiceAccountName {
|
||||
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchesUncachedServiceAccount(t *testing.T) {
|
||||
ns := "myns"
|
||||
|
||||
// Build a test client that the admission plugin can use to look up the service account missing from its cache
|
||||
client := testclient.NewSimpleFake(&api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: DefaultServiceAccountName,
|
||||
Namespace: ns,
|
||||
},
|
||||
})
|
||||
|
||||
admit := NewServiceAccount(client)
|
||||
|
||||
pod := &api.Pod{}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
|
||||
err := admit.Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if pod.Spec.ServiceAccount != DefaultServiceAccountName {
|
||||
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeniesInvalidServiceAccount(t *testing.T) {
|
||||
ns := "myns"
|
||||
|
||||
// Build a test client that the admission plugin can use to look up the service account missing from its cache
|
||||
client := testclient.NewSimpleFake()
|
||||
|
||||
admit := NewServiceAccount(client)
|
||||
|
||||
pod := &api.Pod{}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
|
||||
err := admit.Admit(attrs)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for missing service account, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomountsAPIToken(t *testing.T) {
|
||||
ns := "myns"
|
||||
tokenName := "token-name"
|
||||
serviceAccountName := DefaultServiceAccountName
|
||||
serviceAccountUID := "12345"
|
||||
|
||||
expectedVolume := api.Volume{
|
||||
Name: tokenName,
|
||||
VolumeSource: api.VolumeSource{
|
||||
Secret: &api.SecretVolumeSource{SecretName: tokenName},
|
||||
},
|
||||
}
|
||||
expectedVolumeMount := api.VolumeMount{
|
||||
Name: tokenName,
|
||||
ReadOnly: true,
|
||||
MountPath: DefaultAPITokenMountPath,
|
||||
}
|
||||
|
||||
admit := NewServiceAccount(nil)
|
||||
admit.MountServiceAccountToken = true
|
||||
|
||||
// Add the default service account for the ns with a token into the cache
|
||||
admit.serviceAccounts.Add(&api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: serviceAccountName,
|
||||
Namespace: ns,
|
||||
UID: types.UID(serviceAccountUID),
|
||||
},
|
||||
Secrets: []api.ObjectReference{
|
||||
{Name: tokenName},
|
||||
},
|
||||
})
|
||||
// Add a token for the service account into the cache
|
||||
admit.secrets.Add(&api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: tokenName,
|
||||
Namespace: ns,
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: serviceAccountName,
|
||||
api.ServiceAccountUIDKey: serviceAccountUID,
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{
|
||||
api.ServiceAccountTokenKey: []byte("token-data"),
|
||||
},
|
||||
})
|
||||
|
||||
pod := &api.Pod{
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{},
|
||||
},
|
||||
},
|
||||
}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
|
||||
err := admit.Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if pod.Spec.ServiceAccount != DefaultServiceAccountName {
|
||||
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount)
|
||||
}
|
||||
if len(pod.Spec.Volumes) != 1 {
|
||||
t.Fatalf("Expected 1 volume, got %d", len(pod.Spec.Volumes))
|
||||
}
|
||||
if !reflect.DeepEqual(expectedVolume, pod.Spec.Volumes[0]) {
|
||||
t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolume, pod.Spec.Volumes[0])
|
||||
}
|
||||
if len(pod.Spec.Containers[0].VolumeMounts) != 1 {
|
||||
t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.Containers[0].VolumeMounts))
|
||||
}
|
||||
if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0]) {
|
||||
t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRespectsExistingMount(t *testing.T) {
|
||||
ns := "myns"
|
||||
tokenName := "token-name"
|
||||
serviceAccountName := DefaultServiceAccountName
|
||||
serviceAccountUID := "12345"
|
||||
|
||||
expectedVolumeMount := api.VolumeMount{
|
||||
Name: "my-custom-mount",
|
||||
ReadOnly: false,
|
||||
MountPath: DefaultAPITokenMountPath,
|
||||
}
|
||||
|
||||
admit := NewServiceAccount(nil)
|
||||
admit.MountServiceAccountToken = true
|
||||
|
||||
// Add the default service account for the ns with a token into the cache
|
||||
admit.serviceAccounts.Add(&api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: serviceAccountName,
|
||||
Namespace: ns,
|
||||
UID: types.UID(serviceAccountUID),
|
||||
},
|
||||
Secrets: []api.ObjectReference{
|
||||
{Name: tokenName},
|
||||
},
|
||||
})
|
||||
// Add a token for the service account into the cache
|
||||
admit.secrets.Add(&api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: tokenName,
|
||||
Namespace: ns,
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: serviceAccountName,
|
||||
api.ServiceAccountUIDKey: serviceAccountUID,
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{
|
||||
api.ServiceAccountTokenKey: []byte("token-data"),
|
||||
},
|
||||
})
|
||||
|
||||
// Define a pod with a container that already mounts a volume at the API token path
|
||||
// Admission should respect that
|
||||
// Additionally, no volume should be created if no container is going to use it
|
||||
pod := &api.Pod{
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{
|
||||
VolumeMounts: []api.VolumeMount{
|
||||
expectedVolumeMount,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
|
||||
err := admit.Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if pod.Spec.ServiceAccount != DefaultServiceAccountName {
|
||||
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount)
|
||||
}
|
||||
if len(pod.Spec.Volumes) != 0 {
|
||||
t.Fatalf("Expected 0 volumes (shouldn't create a volume for a secret we don't need), got %d", len(pod.Spec.Volumes))
|
||||
}
|
||||
if len(pod.Spec.Containers[0].VolumeMounts) != 1 {
|
||||
t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.Containers[0].VolumeMounts))
|
||||
}
|
||||
if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0]) {
|
||||
t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowsReferencedSecretVolumes(t *testing.T) {
|
||||
ns := "myns"
|
||||
|
||||
admit := NewServiceAccount(nil)
|
||||
admit.LimitSecretReferences = true
|
||||
|
||||
// Add the default service account for the ns with a secret reference into the cache
|
||||
admit.serviceAccounts.Add(&api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: DefaultServiceAccountName,
|
||||
Namespace: ns,
|
||||
},
|
||||
Secrets: []api.ObjectReference{
|
||||
{Name: "foo"},
|
||||
},
|
||||
})
|
||||
|
||||
pod := &api.Pod{
|
||||
Spec: api.PodSpec{
|
||||
Volumes: []api.Volume{
|
||||
{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
|
||||
err := admit.Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsUnreferencedSecretVolumes(t *testing.T) {
|
||||
ns := "myns"
|
||||
|
||||
admit := NewServiceAccount(nil)
|
||||
admit.LimitSecretReferences = true
|
||||
|
||||
// Add the default service account for the ns into the cache
|
||||
admit.serviceAccounts.Add(&api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: DefaultServiceAccountName,
|
||||
Namespace: ns,
|
||||
},
|
||||
})
|
||||
|
||||
pod := &api.Pod{
|
||||
Spec: api.PodSpec{
|
||||
Volumes: []api.Volume{
|
||||
{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
|
||||
err := admit.Admit(attrs)
|
||||
if err == nil {
|
||||
t.Errorf("Expected rejection for using a secret the service account does not reference")
|
||||
}
|
||||
}
|
19
plugin/pkg/admission/serviceaccount/doc.go
Normal file
19
plugin/pkg/admission/serviceaccount/doc.go
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// serviceaccount enforces all pods having an associated serviceaccount,
|
||||
// and all containers mounting the API token for that serviceaccount at a known location
|
||||
package serviceaccount
|
101
test/e2e/service_accounts.go
Normal file
101
test/e2e/service_accounts.go
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
)
|
||||
|
||||
var _ = Describe("ServiceAccounts", func() {
|
||||
var c *client.Client
|
||||
var ns string
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
c, err = loadClient()
|
||||
expectNoError(err)
|
||||
ns_, err := createTestingNS("service-accounts", c)
|
||||
ns = ns_.Name
|
||||
expectNoError(err)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up the namespace if a non-default one was used
|
||||
if ns != api.NamespaceDefault {
|
||||
By("Cleaning up the namespace")
|
||||
err := c.Namespaces().Delete(ns)
|
||||
expectNoError(err)
|
||||
}
|
||||
})
|
||||
|
||||
It("should mount an API token into pods", func() {
|
||||
var tokenName string
|
||||
var tokenContent string
|
||||
|
||||
// Standard get, update retry loop
|
||||
expectNoError(wait.Poll(time.Millisecond*500, time.Second*10, func() (bool, error) {
|
||||
By("getting the auto-created API token")
|
||||
tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
|
||||
secrets, err := c.Secrets(ns).List(labels.Everything(), tokenSelector)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(secrets.Items) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if len(secrets.Items) > 1 {
|
||||
return false, fmt.Errorf("Expected 1 token secret, got %d", len(secrets.Items))
|
||||
}
|
||||
tokenName = secrets.Items[0].Name
|
||||
tokenContent = string(secrets.Items[0].Data[api.ServiceAccountTokenKey])
|
||||
return true, nil
|
||||
}))
|
||||
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "pod-service-account-" + string(util.NewUUID()),
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: "service-account-test",
|
||||
Image: "kubernetes/mounttest:0.1",
|
||||
Args: []string{
|
||||
fmt.Sprintf("--file_content=%s/%s", serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountTokenKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: api.RestartPolicyNever,
|
||||
},
|
||||
}
|
||||
|
||||
testContainerOutputInNamespace("consume service account token", c, pod, []string{
|
||||
fmt.Sprintf(`content of file "%s/%s": %s`, serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountTokenKey, tokenContent),
|
||||
}, ns)
|
||||
})
|
||||
})
|
565
test/integration/service_account_test.go
Normal file
565
test/integration/service_account_test.go
Normal file
@ -0,0 +1,565 @@
|
||||
// +build integration,!no-etcd
|
||||
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package integration
|
||||
|
||||
// This file tests authentication and (soon) authorization of HTTP requests to a master object.
|
||||
// It does not use the client in pkg/client/... because authentication and authorization needs
|
||||
// to work for any client of the HTTP interface.
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/master"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools/etcdtest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
|
||||
serviceaccountadmission "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/union"
|
||||
)
|
||||
|
||||
const (
|
||||
rootUserName = "root"
|
||||
rootToken = "root-user-token"
|
||||
|
||||
readOnlyServiceAccountName = "ro"
|
||||
readWriteServiceAccountName = "rw"
|
||||
)
|
||||
|
||||
func init() {
|
||||
requireEtcd()
|
||||
}
|
||||
|
||||
func TestServiceAccountAutoCreate(t *testing.T) {
|
||||
c, _, stopFunc := startServiceAccountTestServer(t)
|
||||
defer stopFunc()
|
||||
|
||||
ns := "test-service-account-creation"
|
||||
|
||||
// Create namespace
|
||||
_, err := c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: ns}})
|
||||
if err != nil {
|
||||
t.Fatalf("could not create namespace: %v", err)
|
||||
}
|
||||
|
||||
// Get service account
|
||||
defaultUser, err := getServiceAccount(c, ns, "default", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Default serviceaccount not created: %v", err)
|
||||
}
|
||||
|
||||
// Delete service account
|
||||
err = c.ServiceAccounts(ns).Delete(defaultUser.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not delete default serviceaccount: %v", err)
|
||||
}
|
||||
|
||||
// Get recreated service account
|
||||
defaultUser2, err := getServiceAccount(c, ns, "default", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Default serviceaccount not created: %v", err)
|
||||
}
|
||||
if defaultUser2.UID == defaultUser.UID {
|
||||
t.Fatalf("Expected different UID with recreated serviceaccount")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceAccountTokenAutoCreate(t *testing.T) {
|
||||
c, _, stopFunc := startServiceAccountTestServer(t)
|
||||
defer stopFunc()
|
||||
|
||||
ns := "test-service-account-token-creation"
|
||||
name := "my-service-account"
|
||||
|
||||
// Create namespace
|
||||
_, err := c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: ns}})
|
||||
if err != nil {
|
||||
t.Fatalf("could not create namespace: %v", err)
|
||||
}
|
||||
|
||||
// Create service account
|
||||
serviceAccount, err := c.ServiceAccounts(ns).Create(&api.ServiceAccount{ObjectMeta: api.ObjectMeta{Name: name}})
|
||||
if err != nil {
|
||||
t.Fatalf("Service Account not created: %v", err)
|
||||
}
|
||||
|
||||
// Get token
|
||||
token1Name, token1, err := getReferencedServiceAccountToken(c, ns, name, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Delete token
|
||||
err = c.Secrets(ns).Delete(token1Name)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not delete token: %v", err)
|
||||
}
|
||||
|
||||
// Get recreated token
|
||||
token2Name, token2, err := getReferencedServiceAccountToken(c, ns, name, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if token1Name == token2Name {
|
||||
t.Fatalf("Expected new auto-created token name")
|
||||
}
|
||||
if token1 == token2 {
|
||||
t.Fatalf("Expected new auto-created token value")
|
||||
}
|
||||
|
||||
// Trigger creation of a new referenced token
|
||||
serviceAccount, err = c.ServiceAccounts(ns).Get(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serviceAccount.Secrets = []api.ObjectReference{}
|
||||
_, err = c.ServiceAccounts(ns).Update(serviceAccount)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get rotated token
|
||||
token3Name, token3, err := getReferencedServiceAccountToken(c, ns, name, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if token3Name == token2Name {
|
||||
t.Fatalf("Expected new auto-created token name")
|
||||
}
|
||||
if token3 == token2 {
|
||||
t.Fatalf("Expected new auto-created token value")
|
||||
}
|
||||
|
||||
// Delete service account
|
||||
err = c.ServiceAccounts(ns).Delete(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for tokens to be deleted
|
||||
tokensToCleanup := util.NewStringSet(token1Name, token2Name, token3Name)
|
||||
err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) {
|
||||
// Get all secrets in the namespace
|
||||
secrets, err := c.Secrets(ns).List(labels.Everything(), fields.Everything())
|
||||
// Retrieval errors should fail
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, s := range secrets.Items {
|
||||
if tokensToCleanup.Has(s.Name) {
|
||||
// Still waiting for tokens to be cleaned up
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
// All clean
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Error waiting for tokens to be deleted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceAccountTokenAutoMount(t *testing.T) {
|
||||
c, _, stopFunc := startServiceAccountTestServer(t)
|
||||
defer stopFunc()
|
||||
|
||||
ns := "auto-mount-ns"
|
||||
|
||||
// Create "my" namespace
|
||||
_, err := c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: ns}})
|
||||
if err != nil && !errors.IsAlreadyExists(err) {
|
||||
t.Fatalf("could not create namespace: %v", err)
|
||||
}
|
||||
|
||||
// Get default token
|
||||
defaultTokenName, _, err := getReferencedServiceAccountToken(c, ns, serviceaccountadmission.DefaultServiceAccountName, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Pod to create
|
||||
protoPod := api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{Name: "protopod"},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: "container-1",
|
||||
Image: "container-1-image",
|
||||
},
|
||||
{
|
||||
Name: "container-2",
|
||||
Image: "container-2-image",
|
||||
VolumeMounts: []api.VolumeMount{
|
||||
{Name: "empty-dir", MountPath: serviceaccountadmission.DefaultAPITokenMountPath},
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []api.Volume{
|
||||
{
|
||||
Name: "empty-dir",
|
||||
VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Pod we expect to get created
|
||||
expectedServiceAccount := serviceaccountadmission.DefaultServiceAccountName
|
||||
expectedVolumes := append(protoPod.Spec.Volumes, api.Volume{
|
||||
Name: defaultTokenName,
|
||||
VolumeSource: api.VolumeSource{
|
||||
Secret: &api.SecretVolumeSource{
|
||||
SecretName: defaultTokenName,
|
||||
},
|
||||
},
|
||||
})
|
||||
expectedContainer1VolumeMounts := []api.VolumeMount{
|
||||
{Name: defaultTokenName, MountPath: serviceaccountadmission.DefaultAPITokenMountPath, ReadOnly: true},
|
||||
}
|
||||
expectedContainer2VolumeMounts := protoPod.Spec.Containers[1].VolumeMounts
|
||||
|
||||
createdPod, err := c.Pods(ns).Create(&protoPod)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if createdPod.Spec.ServiceAccount != expectedServiceAccount {
|
||||
t.Fatalf("Expected %s, got %s", expectedServiceAccount, createdPod.Spec.ServiceAccount)
|
||||
}
|
||||
if !api.Semantic.DeepEqual(&expectedVolumes, &createdPod.Spec.Volumes) {
|
||||
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedVolumes, createdPod.Spec.Volumes)
|
||||
}
|
||||
if !api.Semantic.DeepEqual(&expectedContainer1VolumeMounts, &createdPod.Spec.Containers[0].VolumeMounts) {
|
||||
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedContainer1VolumeMounts, createdPod.Spec.Containers[0].VolumeMounts)
|
||||
}
|
||||
if !api.Semantic.DeepEqual(&expectedContainer2VolumeMounts, &createdPod.Spec.Containers[1].VolumeMounts) {
|
||||
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedContainer2VolumeMounts, createdPod.Spec.Containers[1].VolumeMounts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceAccountTokenAuthentication(t *testing.T) {
|
||||
c, config, stopFunc := startServiceAccountTestServer(t)
|
||||
defer stopFunc()
|
||||
|
||||
myns := "auth-ns"
|
||||
otherns := "other-ns"
|
||||
|
||||
// Create "my" namespace
|
||||
_, err := c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: myns}})
|
||||
if err != nil && !errors.IsAlreadyExists(err) {
|
||||
t.Fatalf("could not create namespace: %v", err)
|
||||
}
|
||||
|
||||
// Create "other" namespace
|
||||
_, err = c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: otherns}})
|
||||
if err != nil && !errors.IsAlreadyExists(err) {
|
||||
t.Fatalf("could not create namespace: %v", err)
|
||||
}
|
||||
|
||||
// Create "ro" user in myns
|
||||
_, err = c.ServiceAccounts(myns).Create(&api.ServiceAccount{ObjectMeta: api.ObjectMeta{Name: readOnlyServiceAccountName}})
|
||||
if err != nil {
|
||||
t.Fatalf("Service Account not created: %v", err)
|
||||
}
|
||||
roTokenName, roToken, err := getReferencedServiceAccountToken(c, myns, readOnlyServiceAccountName, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
roClientConfig := config
|
||||
roClientConfig.BearerToken = roToken
|
||||
roClient := client.NewOrDie(&roClientConfig)
|
||||
doServiceAccountAPIRequests(t, roClient, myns, true, true, false)
|
||||
doServiceAccountAPIRequests(t, roClient, otherns, true, false, false)
|
||||
err = c.Secrets(myns).Delete(roTokenName)
|
||||
if err != nil {
|
||||
t.Fatalf("could not delete token: %v", err)
|
||||
}
|
||||
doServiceAccountAPIRequests(t, roClient, myns, false, false, false)
|
||||
|
||||
// Create "rw" user in myns
|
||||
_, err = c.ServiceAccounts(myns).Create(&api.ServiceAccount{ObjectMeta: api.ObjectMeta{Name: readWriteServiceAccountName}})
|
||||
if err != nil {
|
||||
t.Fatalf("Service Account not created: %v", err)
|
||||
}
|
||||
_, rwToken, err := getReferencedServiceAccountToken(c, myns, readWriteServiceAccountName, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rwClientConfig := config
|
||||
rwClientConfig.BearerToken = rwToken
|
||||
rwClient := client.NewOrDie(&rwClientConfig)
|
||||
doServiceAccountAPIRequests(t, rwClient, myns, true, true, true)
|
||||
doServiceAccountAPIRequests(t, rwClient, otherns, true, false, false)
|
||||
|
||||
// Get default user and token which should have been automatically created
|
||||
_, defaultToken, err := getReferencedServiceAccountToken(c, myns, "default", true)
|
||||
if err != nil {
|
||||
t.Fatalf("could not get default user and token: %v", err)
|
||||
}
|
||||
defaultClientConfig := config
|
||||
defaultClientConfig.BearerToken = defaultToken
|
||||
defaultClient := client.NewOrDie(&defaultClientConfig)
|
||||
doServiceAccountAPIRequests(t, defaultClient, myns, true, false, false)
|
||||
}
|
||||
|
||||
// startServiceAccountTestServer returns a started server
|
||||
// It is the responsibility of the caller to ensure the returned stopFunc is called
|
||||
func startServiceAccountTestServer(t *testing.T) (*client.Client, client.Config, func()) {
|
||||
|
||||
deleteAllEtcdKeys()
|
||||
|
||||
// Etcd
|
||||
helper, err := master.NewEtcdHelper(newEtcdClient(), testapi.Version(), etcdtest.PathPrefix())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Listener
|
||||
var m *master.Master
|
||||
apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
m.Handler.ServeHTTP(w, req)
|
||||
}))
|
||||
|
||||
// Anonymous client config
|
||||
clientConfig := client.Config{Host: apiServer.URL, Version: testapi.Version()}
|
||||
// Root client
|
||||
rootClient := client.NewOrDie(&client.Config{Host: apiServer.URL, Version: testapi.Version(), BearerToken: rootToken})
|
||||
|
||||
// Set up two authenticators:
|
||||
// 1. A token authenticator that maps the rootToken to the "root" user
|
||||
// 2. A ServiceAccountToken authenticator that validates ServiceAccount tokens
|
||||
rootTokenAuth := authenticator.TokenFunc(func(token string) (user.Info, bool, error) {
|
||||
if token == rootToken {
|
||||
return &user.DefaultInfo{rootUserName, "", []string{}}, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
})
|
||||
serviceAccountKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
serviceAccountTokenAuth := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{&serviceAccountKey.PublicKey}, true, rootClient)
|
||||
authenticator := union.New(
|
||||
bearertoken.New(rootTokenAuth),
|
||||
bearertoken.New(serviceAccountTokenAuth),
|
||||
)
|
||||
|
||||
// Set up a stub authorizer:
|
||||
// 1. The "root" user is allowed to do anything
|
||||
// 2. ServiceAccounts named "ro" are allowed read-only operations in their namespace
|
||||
// 3. ServiceAccounts named "rw" are allowed any operation in their namespace
|
||||
authorizer := authorizer.AuthorizerFunc(func(attrs authorizer.Attributes) error {
|
||||
username := attrs.GetUserName()
|
||||
ns := attrs.GetNamespace()
|
||||
|
||||
// If the user is "root"...
|
||||
if username == rootUserName {
|
||||
// allow them to do anything
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the user is a service account...
|
||||
if serviceAccountNamespace, serviceAccountName, err := serviceaccount.SplitUsername(username); err == nil {
|
||||
// Limit them to their own namespace
|
||||
if serviceAccountNamespace == ns {
|
||||
switch serviceAccountName {
|
||||
case readOnlyServiceAccountName:
|
||||
if attrs.IsReadOnly() {
|
||||
return nil
|
||||
}
|
||||
case readWriteServiceAccountName:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("User %s is denied (ns=%s, readonly=%v, resource=%s)", username, ns, attrs.IsReadOnly(), attrs.GetResource())
|
||||
})
|
||||
|
||||
// Set up admission plugin to auto-assign serviceaccounts to pods
|
||||
serviceAccountAdmission := serviceaccountadmission.NewServiceAccount(rootClient)
|
||||
|
||||
// Create a master and install handlers into mux.
|
||||
m = master.New(&master.Config{
|
||||
EtcdHelper: helper,
|
||||
KubeletClient: client.FakeKubeletClient{},
|
||||
EnableLogsSupport: false,
|
||||
EnableUISupport: false,
|
||||
EnableIndex: true,
|
||||
APIPrefix: "/api",
|
||||
Authenticator: authenticator,
|
||||
Authorizer: authorizer,
|
||||
AdmissionControl: serviceAccountAdmission,
|
||||
})
|
||||
|
||||
// Start the service account and service account token controllers
|
||||
tokenController := serviceaccount.NewTokensController(rootClient, serviceaccount.DefaultTokenControllerOptions(serviceaccount.JWTTokenGenerator(serviceAccountKey)))
|
||||
tokenController.Run()
|
||||
serviceAccountController := serviceaccount.NewServiceAccountsController(rootClient, serviceaccount.DefaultServiceAccountControllerOptions())
|
||||
serviceAccountController.Run()
|
||||
// Start the admission plugin reflectors
|
||||
serviceAccountAdmission.Run()
|
||||
|
||||
stop := func() {
|
||||
tokenController.Stop()
|
||||
serviceAccountController.Stop()
|
||||
serviceAccountAdmission.Stop()
|
||||
apiServer.Close()
|
||||
}
|
||||
|
||||
return rootClient, clientConfig, stop
|
||||
}
|
||||
|
||||
func getServiceAccount(c *client.Client, ns string, name string, shouldWait bool) (*api.ServiceAccount, error) {
|
||||
if !shouldWait {
|
||||
return c.ServiceAccounts(ns).Get(name)
|
||||
}
|
||||
|
||||
var user *api.ServiceAccount
|
||||
var err error
|
||||
err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) {
|
||||
user, err = c.ServiceAccounts(ns).Get(name)
|
||||
if errors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
return user, err
|
||||
}
|
||||
|
||||
func getReferencedServiceAccountToken(c *client.Client, ns string, name string, shouldWait bool) (string, string, error) {
|
||||
tokenName := ""
|
||||
token := ""
|
||||
|
||||
findToken := func() (bool, error) {
|
||||
user, err := c.ServiceAccounts(ns).Get(name)
|
||||
if errors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, ref := range user.Secrets {
|
||||
secret, err := c.Secrets(ns).Get(ref.Name)
|
||||
if errors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if secret.Type != api.SecretTypeServiceAccountToken {
|
||||
continue
|
||||
}
|
||||
name := secret.Annotations[api.ServiceAccountNameKey]
|
||||
uid := secret.Annotations[api.ServiceAccountUIDKey]
|
||||
tokenData := secret.Data[api.ServiceAccountTokenKey]
|
||||
if name == user.Name && uid == string(user.UID) && len(tokenData) > 0 {
|
||||
tokenName = secret.Name
|
||||
token = string(tokenData)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if shouldWait {
|
||||
err := wait.Poll(time.Second, 10*time.Second, findToken)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
} else {
|
||||
ok, err := findToken()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("No token found for %s/%s", ns, name)
|
||||
}
|
||||
}
|
||||
return tokenName, token, nil
|
||||
}
|
||||
|
||||
type testOperation func() error
|
||||
|
||||
func doServiceAccountAPIRequests(t *testing.T, c *client.Client, ns string, authenticated bool, canRead bool, canWrite bool) {
|
||||
testSecret := &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{Name: "testSecret"},
|
||||
Data: map[string][]byte{"test": []byte("data")},
|
||||
}
|
||||
|
||||
readOps := []testOperation{
|
||||
func() error { _, err := c.Secrets(ns).List(labels.Everything(), fields.Everything()); return err },
|
||||
func() error { _, err := c.Pods(ns).List(labels.Everything(), fields.Everything()); return err },
|
||||
}
|
||||
writeOps := []testOperation{
|
||||
func() error { _, err := c.Secrets(ns).Create(testSecret); return err },
|
||||
func() error { return c.Secrets(ns).Delete(testSecret.Name) },
|
||||
}
|
||||
|
||||
for _, op := range readOps {
|
||||
err := op()
|
||||
unauthorizedError := errors.IsUnauthorized(err)
|
||||
forbiddenError := errors.IsForbidden(err)
|
||||
|
||||
switch {
|
||||
case !authenticated && !unauthorizedError:
|
||||
t.Fatalf("expected unauthorized error, got %v", err)
|
||||
case authenticated && unauthorizedError:
|
||||
t.Fatalf("unexpected unauthorized error: %v", err)
|
||||
case authenticated && canRead && forbiddenError:
|
||||
t.Fatalf("unexpected forbidden error: %v", err)
|
||||
case authenticated && !canRead && !forbiddenError:
|
||||
t.Fatalf("expected forbidden error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, op := range writeOps {
|
||||
err := op()
|
||||
unauthorizedError := errors.IsUnauthorized(err)
|
||||
forbiddenError := errors.IsForbidden(err)
|
||||
|
||||
switch {
|
||||
case !authenticated && !unauthorizedError:
|
||||
t.Fatalf("expected unauthorized error, got %v", err)
|
||||
case authenticated && unauthorizedError:
|
||||
t.Fatalf("unexpected unauthorized error: %v", err)
|
||||
case authenticated && canWrite && forbiddenError:
|
||||
t.Fatalf("unexpected forbidden error: %v", err)
|
||||
case authenticated && !canWrite && !forbiddenError:
|
||||
t.Fatalf("expected forbidden error, got: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user