Skip to content

Commit

Permalink
feat: support basic authorization (#540)
Browse files Browse the repository at this point in the history
## What type of PR is this?
/kind feature

## What this PR does / why we need it:

This PR provides the login functionality, which is essential in a
production environment.

## Which issue(s) this PR fixes:

Fixes #497

---------

Co-authored-by: elliotxx <951376975@qq.com>
Co-authored-by: hai-tian <tianhai_th@163.com>
Co-authored-by: hai-tian <wb-th358723@antgroup.com>
  • Loading branch information
4 people authored Aug 20, 2024
1 parent cd63e91 commit 66ab414
Show file tree
Hide file tree
Showing 41 changed files with 1,548 additions and 676 deletions.
6 changes: 6 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
# /docs/ @doctocat
/docs/ @elliotxx @panshuai-ps @adohe @ffforest @ruquanzhao

# In this example, @doctocat owns any file in the `/ui`
# directory in the root of your repository and any of its
# subdirectories.
# /ui/ @doctocat
/ui/ @elliotxx @ruquanzhao @hai-tian

# In this example, @octocat owns any file in the `/apps`
# directory in the root of your repository except for the `/apps/github`
# subdirectory, as its owners are left empty.
Expand Down
21 changes: 19 additions & 2 deletions cmd/karpor/app/options/recommended.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ package options

import (
"fmt"

"github.com/spf13/pflag"
"time"

karporopenapi "github.com/KusionStack/karpor/pkg/kubernetes/generated/openapi"
k8sopenapi "github.com/KusionStack/karpor/pkg/kubernetes/openapi"
"github.com/KusionStack/karpor/pkg/kubernetes/registry"
"github.com/KusionStack/karpor/pkg/kubernetes/scheme"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
Expand All @@ -39,6 +40,7 @@ import (
"k8s.io/component-base/featuregate"
"k8s.io/kube-openapi/pkg/common"
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
"k8s.io/kubernetes/pkg/serviceaccount"
)

// RecommendedOptions contains the recommended options for running an API server.
Expand All @@ -63,6 +65,10 @@ type RecommendedOptions struct {
EgressSelector *options.EgressSelectorOptions
// Traces contains options to control distributed request tracing.
Traces *options.TracingOptions

ServiceAccountSigningKeyFile string
ServiceAccountIssuer serviceaccount.TokenGenerator
ServiceAccountTokenMaxExpiration time.Duration
}

func NewRecommendedOptions(prefix string, codec runtime.Codec) *RecommendedOptions {
Expand All @@ -77,6 +83,7 @@ func NewRecommendedOptions(prefix string, codec runtime.Codec) *RecommendedOptio
Authentication: kubeoptions.NewBuiltInAuthenticationOptions().
WithAnonymous().
WithClientCert().
WithServiceAccounts().
WithRequestHeader(),
Authorization: kubeoptions.NewBuiltInAuthorizationOptions(),
Audit: options.NewAuditOptions(),
Expand All @@ -100,6 +107,8 @@ func (o *RecommendedOptions) AddFlags(fs *pflag.FlagSet) {
o.Admission.AddFlags(fs)
o.EgressSelector.AddFlags(fs)
o.Traces.AddFlags(fs)
fs.StringVar(&o.ServiceAccountSigningKeyFile, "service-account-signing-key-file", "",
"Path to the file that contains the current private key of the service account token issuer. The issuer will sign issued ID tokens with this private key.")
}

// ApplyTo adds RecommendedOptions to the server configuration.
Expand Down Expand Up @@ -167,6 +176,14 @@ func (o *RecommendedOptions) ApplyTo(config *server.RecommendedConfig) error {
return nil
}

// ApplyToExtraConfig adds RecommendedOptions to the extra server configuration.
func (o *RecommendedOptions) ApplyToExtraConfig(config *registry.ExtraConfig) error {
config.ServiceAccountIssuer = o.ServiceAccountIssuer
config.ServiceAccountMaxExpiration = o.ServiceAccountTokenMaxExpiration
config.ExtendExpiration = o.Authentication.ServiceAccounts.ExtendExpiration
return nil
}

func (o *RecommendedOptions) Validate() []error {
errors := []error{}
errors = append(errors, o.ServerRun.Validate()...)
Expand Down
45 changes: 45 additions & 0 deletions cmd/karpor/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net"
"net/http"
"os"
"time"

"github.com/KusionStack/karpor/cmd/karpor/app/options"
"github.com/KusionStack/karpor/pkg/kubernetes/registry"
Expand All @@ -39,11 +40,16 @@ import (
"k8s.io/apiserver/pkg/features"
genericapiserver "k8s.io/apiserver/pkg/server"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/util/keyutil"
"k8s.io/klog/v2"
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
"k8s.io/kubernetes/pkg/serviceaccount"
netutils "k8s.io/utils/net"
)

const defaultEtcdPathPrefix = "/registry/karpor"
const defaultTokenIssuer = "karpor"
const defaultTokenMaxExpiration = 8760 * time.Hour

// Options contains state for master/api server
type Options struct {
Expand Down Expand Up @@ -144,6 +150,38 @@ func (o *Options) Validate(args []string) error {

// Complete fills in fields required to have valid data
func (o *Options) Complete() error {
// generate token issuer
if len(o.RecommendedOptions.Authentication.ServiceAccounts.Issuers) == 0 || o.RecommendedOptions.Authentication.ServiceAccounts.Issuers[0] == "" {
o.RecommendedOptions.Authentication.ServiceAccounts.Issuers = []string{defaultTokenIssuer}
}

// set default token max expiration
o.RecommendedOptions.ServiceAccountTokenMaxExpiration = defaultTokenMaxExpiration
if o.RecommendedOptions.Authentication.ServiceAccounts.MaxExpiration != 0 {
o.RecommendedOptions.ServiceAccountTokenMaxExpiration = o.RecommendedOptions.Authentication.ServiceAccounts.MaxExpiration
}

// complete two content-related keys with each other
if o.RecommendedOptions.ServiceAccountSigningKeyFile == "" && (len(o.RecommendedOptions.Authentication.ServiceAccounts.KeyFiles) == 0 ||
o.RecommendedOptions.Authentication.ServiceAccounts.KeyFiles[0] == "") {
return fmt.Errorf("no valid serviceaccounts signing key file")
}
if o.RecommendedOptions.ServiceAccountSigningKeyFile == "" {
o.RecommendedOptions.ServiceAccountSigningKeyFile = o.RecommendedOptions.Authentication.ServiceAccounts.KeyFiles[0]
}
if len(o.RecommendedOptions.Authentication.ServiceAccounts.KeyFiles) == 0 {
o.RecommendedOptions.Authentication.ServiceAccounts.KeyFiles = []string{o.RecommendedOptions.ServiceAccountSigningKeyFile}
}

// create token generator
sk, err := keyutil.PrivateKeyFromFile(o.RecommendedOptions.ServiceAccountSigningKeyFile)
if err != nil {
return fmt.Errorf("failed to parse key-file for token generator: %w", err)
}
o.RecommendedOptions.ServiceAccountIssuer, err = serviceaccount.JWTTokenGenerator(o.RecommendedOptions.Authentication.ServiceAccounts.Issuers[0], sk)
if err != nil {
return fmt.Errorf("create token generator failed: %w", err)
}
return nil
}

Expand All @@ -153,9 +191,16 @@ func (o *Options) Config() (*server.Config, error) {
GenericConfig: genericapiserver.NewRecommendedConfig(scheme.Codecs),
ExtraConfig: &registry.ExtraConfig{},
}
// always allow access if readOnlyMode is open
if o.CoreOptions.ReadOnlyMode {
o.RecommendedOptions.Authorization.Modes = []string{authzmodes.ModeAlwaysAllow}
}
if err := o.RecommendedOptions.ApplyTo(config.GenericConfig); err != nil {
return nil, err
}
if err := o.RecommendedOptions.ApplyToExtraConfig(config.ExtraConfig); err != nil {
return nil, err
}
if err := o.SearchStorageOptions.ApplyTo(config.ExtraConfig); err != nil {
return nil, err
}
Expand Down
10 changes: 3 additions & 7 deletions config/default-rbac.yaml → config/default-anonymous-rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@ kind: ClusterRole
metadata:
name: anonymous
rules:
- nonResourceURLs:
- /rest-api/v1/resource-group-rule
- /rest-api/v1/resource-group-rule/*
- /rest-api/v1/cluster
- /rest-api/v1/cluster/*
verbs:
- '*'
- nonResourceURLs:
- /
- /rest-api/*
Expand All @@ -25,6 +18,9 @@ rules:
- /insightDetail/*
- /cluster
- /cluster/*
- /login
- /livez
- /readyz
verbs:
- get
---
Expand Down
33 changes: 33 additions & 0 deletions config/default-karpor-admin-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: karpor-admin
rules:
- nonResourceURLs:
- /rest-api/v1/resource-group-rule
- /rest-api/v1/resource-group-rule/*
- /rest-api/v1/cluster
- /rest-api/v1/cluster/*
verbs:
- '*'
- nonResourceURLs:
- /
- /rest-api/*
- /endpoints
- /public/*
- /docs/*
- /server-configs
- /search
- /search/*
- /insight
- /insight/*
- /insightDetail
- /insightDetail/*
- /cluster
- /cluster/*
- /login
- /rest-api/v1/authn
- /livez
- /readyz
verbs:
- get
30 changes: 30 additions & 0 deletions config/default-karpor-guest-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: karpor-guest
rules:
- nonResourceURLs:
- /rest-api/v1/resource-group-rule
- /rest-api/v1/resource-group-rule/*
- /rest-api/v1/cluster
- /rest-api/v1/cluster/*
- /rest-api/v1/authn
- /
- /rest-api/*
- /endpoints
- /public/*
- /docs/*
- /server-configs
- /search
- /search/*
- /insight
- /insight/*
- /insightDetail
- /insightDetail/*
- /cluster
- /cluster/*
- /login
- /livez
- /readyz
verbs:
- get
12 changes: 9 additions & 3 deletions config/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ package config

import _ "embed"

var DefaultConfig = [][]byte{DefaultSyncStrategy, DefaultRBAC}
var DefaultConfig = [][]byte{DefaultSyncStrategy, DefaultAnonymousRBAC, DefaultGuestRBAC, DefaultAdminRBAC}

//go:embed default-rbac.yaml
var DefaultRBAC []byte
//go:embed default-anonymous-rbac.yaml
var DefaultAnonymousRBAC []byte

//go:embed default-karpor-admin-rbac.yaml
var DefaultAdminRBAC []byte

//go:embed default-karpor-guest-rbac.yaml
var DefaultGuestRBAC []byte

//go:embed default-relationship.yaml
var DefaultRelationship []byte
Expand Down
42 changes: 42 additions & 0 deletions pkg/core/handler/authn/authn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright The Karpor Authors.
//
// 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 authn

import (
"net/http"

"github.com/KusionStack/karpor/pkg/core/handler"
)

// Get returns an HTTP handler that determine whether the user's token can pass authentication.
//
// @Summary Get returns an authn result of user's token.
// @Description This endpoint returns an authn result.
// @Tags authn
// @Produce json
// @Success 200 {string} string "OK"
// @Failure 400 {string} string "Bad Request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 404 {string} string "Not Found"
// @Failure 405 {string} string "Method Not Allowed"
// @Failure 429 {string} string "Too Many Requests"
// @Failure 500 {string} string "Internal Server Error"
// @Router /authn [get]
func Get() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// No action is taken as the API server will handle authentication.
handler.HandleResult(w, r, r.Context(), nil, "")
}
}
4 changes: 3 additions & 1 deletion pkg/core/route/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package route
import (
"expvar"
docs "github.com/KusionStack/karpor/api/openapispec"
authnhandler "github.com/KusionStack/karpor/pkg/core/handler/authn"
clusterhandler "github.com/KusionStack/karpor/pkg/core/handler/cluster"
detailhandler "github.com/KusionStack/karpor/pkg/core/handler/detail"
endpointhandler "github.com/KusionStack/karpor/pkg/core/handler/endpoint"
Expand Down Expand Up @@ -109,7 +110,7 @@ func NewCoreRoute(
// Endpoint to list all available endpoints in the router.
router.Get("/endpoints", endpointhandler.Endpoints(router))

// Endpoint to list all available endpoints in the router.
// Expose server configuration and runtime statistics.
router.Get("/server-configs", expvar.Handler().ServeHTTP)

healthhandler.Register(router, generalStorage)
Expand Down Expand Up @@ -165,4 +166,5 @@ func setupRestAPIV1(
})
r.Get("/resource-group-rules", resourcegrouprulehandler.List(resourceGroupMgr))
r.Get("/resource-groups/{resourceGroupRuleName}", resourcegrouphandler.List(resourceGroupMgr))
r.Get("/authn", authnhandler.Get())
}
Loading

0 comments on commit 66ab414

Please sign in to comment.