From 3e67cf8b9bbeb143c928d428d3fec26eb0b251e8 Mon Sep 17 00:00:00 2001 From: mbohlool Date: Mon, 17 Oct 2016 21:48:17 -0700 Subject: [PATCH] Add authentication to openapi Spec --- cmd/kube-apiserver/app/server.go | 3 +- .../cmd/federation-apiserver/app/server.go | 3 +- hack/update-federation-openapi-spec.sh | 4 ++ hack/update-openapi-spec.sh | 3 ++ pkg/apiserver/authenticator/authn.go | 54 +++++++++++++++---- pkg/genericapiserver/config.go | 23 ++++++++ pkg/genericapiserver/openapi/common/common.go | 14 +++++ pkg/genericapiserver/openapi/openapi.go | 12 +++++ 8 files changed, 103 insertions(+), 13 deletions(-) diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index d47dc007c63..4e041ec647b 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -216,7 +216,7 @@ func Run(s *options.APIServer) error { serviceAccountGetter = serviceaccountcontroller.NewGetterFromStorageInterface(storageConfig, storageFactory.ResourcePrefix(api.Resource("serviceaccounts")), storageFactory.ResourcePrefix(api.Resource("secrets"))) } - apiAuthenticator, err := authenticator.New(authenticator.AuthenticatorConfig{ + apiAuthenticator, securityDefinitions, err := authenticator.New(authenticator.AuthenticatorConfig{ Anonymous: s.AnonymousAuth, AnyToken: s.EnableAnyToken, BasicAuthFile: s.BasicAuthFile, @@ -309,6 +309,7 @@ func Run(s *options.APIServer) error { genericConfig.OpenAPIConfig.Definitions = generatedopenapi.OpenAPIDefinitions genericConfig.OpenAPIConfig.GetOperationID = openapi.GetOperationID genericConfig.EnableOpenAPISupport = true + genericConfig.OpenAPIConfig.SecurityDefinitions = securityDefinitions config := &master.Config{ GenericConfig: genericConfig.Config, diff --git a/federation/cmd/federation-apiserver/app/server.go b/federation/cmd/federation-apiserver/app/server.go index c97cd7203e7..495372d285c 100644 --- a/federation/cmd/federation-apiserver/app/server.go +++ b/federation/cmd/federation-apiserver/app/server.go @@ -117,7 +117,7 @@ func Run(s *options.ServerRunOptions) error { storageFactory.SetEtcdLocation(groupResource, servers) } - apiAuthenticator, err := authenticator.New(authenticator.AuthenticatorConfig{ + apiAuthenticator, securityDefinitions, err := authenticator.New(authenticator.AuthenticatorConfig{ Anonymous: s.AnonymousAuth, AnyToken: s.EnableAnyToken, BasicAuthFile: s.BasicAuthFile, @@ -200,6 +200,7 @@ func Run(s *options.ServerRunOptions) error { // this method does not provide good operation IDs for federation, we should create federation's own GetOperationID. genericConfig.OpenAPIConfig.GetOperationID = apiserveropenapi.GetOperationID genericConfig.EnableOpenAPISupport = true + genericConfig.OpenAPIConfig.SecurityDefinitions = securityDefinitions // TODO: Move this to generic api server (Need to move the command line flag). if s.EnableWatchCache { diff --git a/hack/update-federation-openapi-spec.sh b/hack/update-federation-openapi-spec.sh index 86b56604d62..df291f56320 100755 --- a/hack/update-federation-openapi-spec.sh +++ b/hack/update-federation-openapi-spec.sh @@ -42,6 +42,7 @@ trap cleanup EXIT SIGINT kube::golang::setup_env +TMP_DIR=$(mktemp -d /tmp/update-federation-openapi-spec.XXXX) ETCD_HOST=${ETCD_HOST:-127.0.0.1} ETCD_PORT=${ETCD_PORT:-2379} API_PORT=${API_PORT:-8050} @@ -49,6 +50,8 @@ API_HOST=${API_HOST:-127.0.0.1} kube::etcd::start +echo "dummy_token,admin,admin" > $TMP_DIR/tokenauth.csv + # Start federation-apiserver kube::log::status "Starting federation-apiserver" "${KUBE_OUTPUT_HOSTBIN}/hyperkube" federation-apiserver \ @@ -57,6 +60,7 @@ kube::log::status "Starting federation-apiserver" --insecure-port="${API_PORT}" \ --etcd-servers="http://${ETCD_HOST}:${ETCD_PORT}" \ --advertise-address="10.10.10.10" \ + --token-auth-file=$TMP_DIR/tokenauth.csv \ --service-cluster-ip-range="10.0.0.0/24" >/tmp/openapi-federation-api-server.log 2>&1 & APISERVER_PID=$! kube::util::wait_for_url "${API_HOST}:${API_PORT}/" "apiserver: " diff --git a/hack/update-openapi-spec.sh b/hack/update-openapi-spec.sh index b379f161597..e5a4a17ee2c 100755 --- a/hack/update-openapi-spec.sh +++ b/hack/update-openapi-spec.sh @@ -52,6 +52,8 @@ API_HOST=${API_HOST:-127.0.0.1} kube::etcd::start +echo "dummy_token,admin,admin" > $TMP_DIR/tokenauth.csv + # Start kube-apiserver kube::log::status "Starting kube-apiserver" "${KUBE_OUTPUT_HOSTBIN}/kube-apiserver" \ @@ -61,6 +63,7 @@ kube::log::status "Starting kube-apiserver" --etcd-servers="http://${ETCD_HOST}:${ETCD_PORT}" \ --advertise-address="10.10.10.10" \ --cert-dir="${TMP_DIR}/certs" \ + --token-auth-file=$TMP_DIR/tokenauth.csv \ --service-cluster-ip-range="10.0.0.0/24" >/tmp/openapi-api-server.log 2>&1 & APISERVER_PID=$! diff --git a/pkg/apiserver/authenticator/authn.go b/pkg/apiserver/authenticator/authn.go index 042696e9d6b..2dfe01acd71 100644 --- a/pkg/apiserver/authenticator/authn.go +++ b/pkg/apiserver/authenticator/authn.go @@ -19,6 +19,8 @@ package authenticator import ( "time" + "github.com/go-openapi/spec" + "k8s.io/kubernetes/pkg/auth/authenticator" "k8s.io/kubernetes/pkg/auth/authenticator/bearertoken" "k8s.io/kubernetes/pkg/auth/group" @@ -58,30 +60,35 @@ type AuthenticatorConfig struct { // New returns an authenticator.Request or an error that supports the standard // Kubernetes authentication mechanisms. -func New(config AuthenticatorConfig) (authenticator.Request, error) { +func New(config AuthenticatorConfig) (authenticator.Request, *spec.SecurityDefinitions, error) { var authenticators []authenticator.Request + securityDefinitions := spec.SecurityDefinitions{} + hasBasicAuth := false + hasTokenAuth := false // BasicAuth methods, local first, then remote if len(config.BasicAuthFile) > 0 { basicAuth, err := newAuthenticatorFromBasicAuthFile(config.BasicAuthFile) if err != nil { - return nil, err + return nil, nil, err } authenticators = append(authenticators, basicAuth) + hasBasicAuth = true } if len(config.KeystoneURL) > 0 { keystoneAuth, err := newAuthenticatorFromKeystoneURL(config.KeystoneURL) if err != nil { - return nil, err + return nil, nil, err } authenticators = append(authenticators, keystoneAuth) + hasBasicAuth = true } // X509 methods if len(config.ClientCAFile) > 0 { certAuth, err := newAuthenticatorFromClientCAFile(config.ClientCAFile) if err != nil { - return nil, err + return nil, nil, err } authenticators = append(authenticators, certAuth) } @@ -90,16 +97,18 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { if len(config.TokenAuthFile) > 0 { tokenAuth, err := newAuthenticatorFromTokenFile(config.TokenAuthFile) if err != nil { - return nil, err + return nil, nil, err } authenticators = append(authenticators, tokenAuth) + hasTokenAuth = true } if len(config.ServiceAccountKeyFiles) > 0 { serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.ServiceAccountTokenGetter) if err != nil { - return nil, err + return nil, nil, err } authenticators = append(authenticators, serviceAccountAuth) + hasTokenAuth = true } // NOTE(ericchiang): Keep the OpenID Connect after Service Accounts. // @@ -110,32 +119,55 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 { oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(config.OIDCIssuerURL, config.OIDCClientID, config.OIDCCAFile, config.OIDCUsernameClaim, config.OIDCGroupsClaim) if err != nil { - return nil, err + return nil, nil, err } authenticators = append(authenticators, oidcAuth) + hasTokenAuth = true } if len(config.WebhookTokenAuthnConfigFile) > 0 { webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL) if err != nil { - return nil, err + return nil, nil, err } authenticators = append(authenticators, webhookTokenAuth) + hasTokenAuth = true } // always add anytoken last, so that every other token authenticator gets to try first if config.AnyToken { authenticators = append(authenticators, bearertoken.New(anytoken.AnyTokenAuthenticator{})) + hasTokenAuth = true + } + + if hasBasicAuth { + securityDefinitions["HTTPBasic"] = &spec.SecurityScheme{ + SecuritySchemeProps: spec.SecuritySchemeProps{ + Type: "basic", + Description: "HTTP Basic authentication", + }, + } + } + + if hasTokenAuth { + securityDefinitions["BearerToken"] = &spec.SecurityScheme{ + SecuritySchemeProps: spec.SecuritySchemeProps{ + Type: "apiKey", + Name: "authorization", + In: "header", + Description: "Bearer Token authentication", + }, + } } if len(authenticators) == 0 { if config.Anonymous { - return anonymous.NewAuthenticator(), nil + return anonymous.NewAuthenticator(), &securityDefinitions, nil } } switch len(authenticators) { case 0: - return nil, nil + return nil, &securityDefinitions, nil } authenticator := union.New(authenticators...) @@ -147,7 +179,7 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { authenticator = union.NewFailOnError(authenticator, anonymous.NewAuthenticator()) } - return authenticator, nil + return authenticator, &securityDefinitions, nil } // IsValidServiceAccountKeyFile returns true if a valid public RSA key can be read from the given file diff --git a/pkg/genericapiserver/config.go b/pkg/genericapiserver/config.go index 066978b06a5..d2d0bb4af48 100644 --- a/pkg/genericapiserver/config.go +++ b/pkg/genericapiserver/config.go @@ -24,6 +24,7 @@ import ( "os" "path" "regexp" + "sort" "strconv" "strings" "time" @@ -331,6 +332,28 @@ func (c *Config) Complete() completedConfig { } c.ExternalHost = hostAndPort } + // All APIs will have the same authentication for now. + if c.OpenAPIConfig != nil && c.OpenAPIConfig.SecurityDefinitions != nil { + c.OpenAPIConfig.DefaultSecurity = []map[string][]string{} + keys := []string{} + for k := range *c.OpenAPIConfig.SecurityDefinitions { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + c.OpenAPIConfig.DefaultSecurity = append(c.OpenAPIConfig.DefaultSecurity, map[string][]string{k: {}}) + } + if c.OpenAPIConfig.CommonResponses == nil { + c.OpenAPIConfig.CommonResponses = map[int]spec.Response{} + } + if _, exists := c.OpenAPIConfig.CommonResponses[http.StatusUnauthorized]; !exists { + c.OpenAPIConfig.CommonResponses[http.StatusUnauthorized] = spec.Response{ + ResponseProps: spec.ResponseProps{ + Description: "Unauthorized", + }, + } + } + } return completedConfig{c} } diff --git a/pkg/genericapiserver/openapi/common/common.go b/pkg/genericapiserver/openapi/common/common.go index 3bcc744dfad..b639ecbd308 100644 --- a/pkg/genericapiserver/openapi/common/common.go +++ b/pkg/genericapiserver/openapi/common/common.go @@ -45,9 +45,15 @@ type Config struct { // Info is general information about the API. Info *spec.Info + // DefaultResponse will be used if an operation does not have any responses listed. It // will show up as ... "responses" : {"default" : $DefaultResponse} in the spec. DefaultResponse *spec.Response + + // CommonResponses will be added as a response to all operation specs. This is a good place to add common + // responses such as authorization failed. + CommonResponses map[int]spec.Response + // List of webservice's path prefixes to ignore IgnorePrefixes []string @@ -57,6 +63,14 @@ type Config struct { // GetOperationID returns operation id for a restful route. It is an optional function to customize operation IDs. GetOperationID func(servePath string, r *restful.Route) (string, error) + + // SecurityDefinitions is list of all security definitions for OpenAPI service. If this is not nil, the user of config + // is responsible to provide DefaultSecurity and (maybe) add unauthorized response to CommonResponses. + SecurityDefinitions *spec.SecurityDefinitions + + // DefaultSecurity for all operations. This will pass as spec.SwaggerProps.Security to OpenAPI. + // For most cases, this will be list of acceptable definitions in SecurityDefinitions. + DefaultSecurity []map[string][]string } // This function is a reference for converting go (or any custom type) to a simple open API type,format pair. There are diff --git a/pkg/genericapiserver/openapi/openapi.go b/pkg/genericapiserver/openapi/openapi.go index b251272ebb9..fbe6ad13978 100644 --- a/pkg/genericapiserver/openapi/openapi.go +++ b/pkg/genericapiserver/openapi/openapi.go @@ -78,10 +78,17 @@ func (o *openAPI) init(webServices []*restful.WebService) error { return r.Operation, nil } } + if o.config.CommonResponses == nil { + o.config.CommonResponses = map[int]spec.Response{} + } err := o.buildPaths(webServices) if err != nil { return err } + if o.config.SecurityDefinitions != nil { + o.swagger.SecurityDefinitions = *o.config.SecurityDefinitions + o.swagger.Security = o.config.DefaultSecurity + } return nil } @@ -227,6 +234,11 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map return ret, err } } + for code, resp := range o.config.CommonResponses { + if _, exists := ret.Responses.StatusCodeResponses[code]; !exists { + ret.Responses.StatusCodeResponses[code] = resp + } + } // If there is still no response, use default response provided. if len(ret.Responses.StatusCodeResponses) == 0 { ret.Responses.Default = o.config.DefaultResponse