Merge pull request #606 from ericchiang/dev-self-managed-third-party-resources
dev branch: self managed third party resources
This commit is contained in:
commit
6a9df8ab1c
6 changed files with 146 additions and 96 deletions
|
@ -28,7 +28,6 @@ ConfigMap for dex to use. These run dex as a deployment with configuration and
|
||||||
storage, allowing it to get started.
|
storage, allowing it to get started.
|
||||||
|
|
||||||
```
|
```
|
||||||
kubectl create -f thirdpartyresources.yaml
|
|
||||||
kubectl create configmap dex-config --from-file=config.yaml=config-k8s.yaml
|
kubectl create configmap dex-config --from-file=config.yaml=config-k8s.yaml
|
||||||
kubectl create -f deployment.yaml
|
kubectl create -f deployment.yaml
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
# NOTE: Because of a bug in third party resources, each resource must be in it's
|
|
||||||
# own API Group.
|
|
||||||
#
|
|
||||||
# See fix at https://github.com/kubernetes/kubernetes/pull/28414
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
name: auth-code.authcodes.oidc.coreos.com
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: ThirdPartyResource
|
|
||||||
description: "A code which can be claimed for an access token."
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
---
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
name: auth-request.authrequests.oidc.coreos.com
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: ThirdPartyResource
|
|
||||||
description: "A request for an end user to authorize a client."
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
---
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
name: o-auth2-client.oauth2clients.oidc.coreos.com
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: ThirdPartyResource
|
|
||||||
description: "An OpenID Connect client."
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
---
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
name: signing-key.signingkeies.oidc.coreos.com
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: ThirdPartyResource
|
|
||||||
description: "Keys used to sign and verify OpenID Connect tokens."
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
---
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
name: refresh-token.refreshtokens.oidc.coreos.com
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: ThirdPartyResource
|
|
||||||
description: "Refresh tokens for clients to continuously act on behalf of an end user."
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
---
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
name: password.passwords.oidc.coreos.com
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: ThirdPartyResource
|
|
||||||
description: "Passwords managed by the OIDC server."
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gtank/cryptopasta"
|
"github.com/gtank/cryptopasta"
|
||||||
|
"golang.org/x/net/context"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"github.com/coreos/dex/storage"
|
"github.com/coreos/dex/storage"
|
||||||
|
@ -27,27 +28,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
baseURL string
|
baseURL string
|
||||||
namespace string
|
namespace string
|
||||||
|
|
||||||
|
// API version of the oidc resources. For example "oidc.coreos.com". This is
|
||||||
|
// currently not configurable, but could be in the future.
|
||||||
apiVersion string
|
apiVersion string
|
||||||
|
|
||||||
now func() time.Time
|
// This is called once the client's Close method is called to signal goroutines,
|
||||||
|
// such as the one creating third party resources, to stop.
|
||||||
// BUG: currently each third party API group can only have one resource in it,
|
cancel context.CancelFunc
|
||||||
// so for each resource this storage uses, it need a unique API group.
|
|
||||||
//
|
|
||||||
// Prepend the name of each resource to the API group for a predictable mapping.
|
|
||||||
//
|
|
||||||
// See: https://github.com/kubernetes/kubernetes/pull/28414
|
|
||||||
prependResourceNameToAPIGroup bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *client) apiVersionForResource(resource string) string {
|
|
||||||
if !c.prependResourceNameToAPIGroup {
|
|
||||||
return c.apiVersion
|
|
||||||
}
|
|
||||||
return resource + "." + c.apiVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
|
func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
|
||||||
|
@ -56,10 +47,6 @@ func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
|
||||||
basePath = "api/"
|
basePath = "api/"
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.prependResourceNameToAPIGroup && apiVersion != "" && resource != "" {
|
|
||||||
apiVersion = resource + "." + apiVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
var p string
|
var p string
|
||||||
if namespace != "" {
|
if namespace != "" {
|
||||||
p = path.Join(basePath, apiVersion, "namespaces", namespace, resource, name)
|
p = path.Join(basePath, apiVersion, "namespaces", namespace, resource, name)
|
||||||
|
@ -72,15 +59,28 @@ func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
|
||||||
return c.baseURL + "/" + p
|
return c.baseURL + "/" + p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define an error interface so we can get at the underlying status code if it's
|
||||||
|
// absolutely necessary. For instance when we need to see if an error indicates
|
||||||
|
// a resource already exists.
|
||||||
|
type httpError interface {
|
||||||
|
StatusCode() int
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ httpError = (*httpErr)(nil)
|
||||||
|
|
||||||
type httpErr struct {
|
type httpErr struct {
|
||||||
method string
|
method string
|
||||||
url string
|
url string
|
||||||
status string
|
status int
|
||||||
body []byte
|
body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *httpErr) StatusCode() int {
|
||||||
|
return e.status
|
||||||
|
}
|
||||||
|
|
||||||
func (e *httpErr) Error() string {
|
func (e *httpErr) Error() string {
|
||||||
return fmt.Sprintf("%s %s %s: response from server \"%s\"", e.method, e.url, e.status, bytes.TrimSpace(e.body))
|
return fmt.Sprintf("%s %s %s: response from server \"%s\"", e.method, e.url, http.StatusText(e.status), bytes.TrimSpace(e.body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkHTTPErr(r *http.Response, validStatusCodes ...int) error {
|
func checkHTTPErr(r *http.Response, validStatusCodes ...int) error {
|
||||||
|
@ -100,7 +100,7 @@ func checkHTTPErr(r *http.Response, validStatusCodes ...int) error {
|
||||||
method = r.Request.Method
|
method = r.Request.Method
|
||||||
url = r.Request.URL.String()
|
url = r.Request.URL.String()
|
||||||
}
|
}
|
||||||
err = &httpErr{method, url, r.Status, body}
|
err = &httpErr{method, url, r.StatusCode, body}
|
||||||
log.Printf("%s", err)
|
log.Printf("%s", err)
|
||||||
|
|
||||||
if r.StatusCode == http.StatusNotFound {
|
if r.StatusCode == http.StatusNotFound {
|
||||||
|
@ -134,12 +134,16 @@ func (c *client) list(resource string, v interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) post(resource string, v interface{}) error {
|
func (c *client) post(resource string, v interface{}) error {
|
||||||
|
return c.postResource(c.apiVersion, c.namespace, resource, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) postResource(apiVersion, namespace, resource string, v interface{}) error {
|
||||||
body, err := json.Marshal(v)
|
body, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal object: %v", err)
|
return fmt.Errorf("marshal object: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
url := c.urlFor(c.apiVersion, c.namespace, resource, "")
|
url := c.urlFor(apiVersion, namespace, resource, "")
|
||||||
resp, err := c.client.Post(url, "application/json", bytes.NewReader(body))
|
resp, err := c.client.Post(url, "application/json", bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -277,8 +281,6 @@ func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string) (
|
||||||
baseURL: cluster.Server,
|
baseURL: cluster.Server,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
apiVersion: "oidc.coreos.com/v1",
|
apiVersion: "oidc.coreos.com/v1",
|
||||||
now: time.Now,
|
|
||||||
prependResourceNameToAPIGroup: true,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,13 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/coreos/dex/storage"
|
"github.com/coreos/dex/storage"
|
||||||
"github.com/coreos/dex/storage/kubernetes/k8sapi"
|
"github.com/coreos/dex/storage/kubernetes/k8sapi"
|
||||||
|
@ -45,7 +47,6 @@ func (c *Config) Open() (storage.Storage, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli, nil
|
return cli, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,10 +82,57 @@ func (c *Config) open() (*client, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newClient(cluster, user, namespace)
|
cli, err := newClient(cluster, user, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't try to synchronize this because creating third party resources is not
|
||||||
|
// a synchronous event. Even after the API server returns a 200, it can still
|
||||||
|
// take several seconds for them to actually appear.
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
if err := cli.createThirdPartyResources(); err != nil {
|
||||||
|
log.Printf("failed creating third party resources: %v", err)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(30 * time.Second):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If the client is closed, stop trying to create third party resources.
|
||||||
|
cli.cancel = cancel
|
||||||
|
return cli, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *client) createThirdPartyResources() error {
|
||||||
|
for _, r := range thirdPartyResources {
|
||||||
|
err := cli.postResource("extensions/v1beta1", "", "thirdpartyresources", r)
|
||||||
|
if err != nil {
|
||||||
|
if e, ok := err.(httpError); ok {
|
||||||
|
if e.StatusCode() == http.StatusConflict {
|
||||||
|
log.Printf("third party resource already created %q", r.ObjectMeta.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("create third party resource %q", r.ObjectMeta.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *client) Close() error {
|
func (cli *client) Close() error {
|
||||||
|
if cli.cancel != nil {
|
||||||
|
cli.cancel()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +156,7 @@ func (cli *client) CreateRefresh(r storage.RefreshToken) error {
|
||||||
refresh := RefreshToken{
|
refresh := RefreshToken{
|
||||||
TypeMeta: k8sapi.TypeMeta{
|
TypeMeta: k8sapi.TypeMeta{
|
||||||
Kind: kindRefreshToken,
|
Kind: kindRefreshToken,
|
||||||
APIVersion: cli.apiVersionForResource(resourceRefreshToken),
|
APIVersion: cli.apiVersion,
|
||||||
},
|
},
|
||||||
ObjectMeta: k8sapi.ObjectMeta{
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
Name: r.RefreshToken,
|
Name: r.RefreshToken,
|
||||||
|
|
|
@ -60,7 +60,7 @@ func TestURLFor(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
c := &client{baseURL: test.baseURL, prependResourceNameToAPIGroup: false}
|
c := &client{baseURL: test.baseURL}
|
||||||
got := c.urlFor(test.apiVersion, test.namespace, test.resource, test.name)
|
got := c.urlFor(test.apiVersion, test.namespace, test.resource, test.name)
|
||||||
if got != test.want {
|
if got != test.want {
|
||||||
t.Errorf("(&client{baseURL:%q}).urlFor(%q, %q, %q, %q): expected %q got %q",
|
t.Errorf("(&client{baseURL:%q}).urlFor(%q, %q, %q, %q): expected %q got %q",
|
||||||
|
|
|
@ -11,6 +11,64 @@ import (
|
||||||
"github.com/coreos/dex/storage/kubernetes/k8sapi"
|
"github.com/coreos/dex/storage/kubernetes/k8sapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var tprMeta = k8sapi.TypeMeta{
|
||||||
|
APIVersion: "extensions/v1beta1",
|
||||||
|
Kind: "ThirdPartyResource",
|
||||||
|
}
|
||||||
|
|
||||||
|
// The set of third party resources required by the storage. These are managed by
|
||||||
|
// the storage so it can migrate itself by creating new resources.
|
||||||
|
var thirdPartyResources = []k8sapi.ThirdPartyResource{
|
||||||
|
{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "auth-code.oidc.coreos.com",
|
||||||
|
},
|
||||||
|
TypeMeta: tprMeta,
|
||||||
|
Description: "A code which can be claimed for an access token.",
|
||||||
|
Versions: []k8sapi.APIVersion{{Name: "v1"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "auth-request.oidc.coreos.com",
|
||||||
|
},
|
||||||
|
TypeMeta: tprMeta,
|
||||||
|
Description: "A request for an end user to authorize a client.",
|
||||||
|
Versions: []k8sapi.APIVersion{{Name: "v1"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "o-auth2-client.oidc.coreos.com",
|
||||||
|
},
|
||||||
|
TypeMeta: tprMeta,
|
||||||
|
Description: "An OpenID Connect client.",
|
||||||
|
Versions: []k8sapi.APIVersion{{Name: "v1"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "signing-key.oidc.coreos.com",
|
||||||
|
},
|
||||||
|
TypeMeta: tprMeta,
|
||||||
|
Description: "Keys used to sign and verify OpenID Connect tokens.",
|
||||||
|
Versions: []k8sapi.APIVersion{{Name: "v1"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "refresh-token.oidc.coreos.com",
|
||||||
|
},
|
||||||
|
TypeMeta: tprMeta,
|
||||||
|
Description: "Refresh tokens for clients to continuously act on behalf of an end user.",
|
||||||
|
Versions: []k8sapi.APIVersion{{Name: "v1"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "password.oidc.coreos.com",
|
||||||
|
},
|
||||||
|
TypeMeta: tprMeta,
|
||||||
|
Description: "Passwords managed by the OIDC server.",
|
||||||
|
Versions: []k8sapi.APIVersion{{Name: "v1"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// There will only ever be a single keys resource. Maintain this by setting a
|
// There will only ever be a single keys resource. Maintain this by setting a
|
||||||
// common name.
|
// common name.
|
||||||
const keysName = "openid-connect-keys"
|
const keysName = "openid-connect-keys"
|
||||||
|
@ -45,7 +103,7 @@ func (cli *client) fromStorageClient(c storage.Client) Client {
|
||||||
return Client{
|
return Client{
|
||||||
TypeMeta: k8sapi.TypeMeta{
|
TypeMeta: k8sapi.TypeMeta{
|
||||||
Kind: kindClient,
|
Kind: kindClient,
|
||||||
APIVersion: cli.apiVersionForResource(resourceClient),
|
APIVersion: cli.apiVersion,
|
||||||
},
|
},
|
||||||
ObjectMeta: k8sapi.ObjectMeta{
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
Name: c.ID,
|
Name: c.ID,
|
||||||
|
@ -162,7 +220,7 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
|
||||||
req := AuthRequest{
|
req := AuthRequest{
|
||||||
TypeMeta: k8sapi.TypeMeta{
|
TypeMeta: k8sapi.TypeMeta{
|
||||||
Kind: kindAuthRequest,
|
Kind: kindAuthRequest,
|
||||||
APIVersion: cli.apiVersionForResource(resourceAuthRequest),
|
APIVersion: cli.apiVersion,
|
||||||
},
|
},
|
||||||
ObjectMeta: k8sapi.ObjectMeta{
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
Name: a.ID,
|
Name: a.ID,
|
||||||
|
@ -216,7 +274,7 @@ func (cli *client) fromStoragePassword(p storage.Password) Password {
|
||||||
return Password{
|
return Password{
|
||||||
TypeMeta: k8sapi.TypeMeta{
|
TypeMeta: k8sapi.TypeMeta{
|
||||||
Kind: kindPassword,
|
Kind: kindPassword,
|
||||||
APIVersion: cli.apiVersionForResource(resourcePassword),
|
APIVersion: cli.apiVersion,
|
||||||
},
|
},
|
||||||
ObjectMeta: k8sapi.ObjectMeta{
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
Name: emailToID(email),
|
Name: emailToID(email),
|
||||||
|
@ -270,7 +328,7 @@ func (cli *client) fromStorageAuthCode(a storage.AuthCode) AuthCode {
|
||||||
return AuthCode{
|
return AuthCode{
|
||||||
TypeMeta: k8sapi.TypeMeta{
|
TypeMeta: k8sapi.TypeMeta{
|
||||||
Kind: kindAuthCode,
|
Kind: kindAuthCode,
|
||||||
APIVersion: cli.apiVersionForResource(resourceAuthCode),
|
APIVersion: cli.apiVersion,
|
||||||
},
|
},
|
||||||
ObjectMeta: k8sapi.ObjectMeta{
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
Name: a.ID,
|
Name: a.ID,
|
||||||
|
@ -346,7 +404,7 @@ func (cli *client) fromStorageKeys(keys storage.Keys) Keys {
|
||||||
return Keys{
|
return Keys{
|
||||||
TypeMeta: k8sapi.TypeMeta{
|
TypeMeta: k8sapi.TypeMeta{
|
||||||
Kind: kindKeys,
|
Kind: kindKeys,
|
||||||
APIVersion: cli.apiVersionForResource(resourceKeys),
|
APIVersion: cli.apiVersion,
|
||||||
},
|
},
|
||||||
ObjectMeta: k8sapi.ObjectMeta{
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
Name: keysName,
|
Name: keysName,
|
||||||
|
|
Reference in a new issue