Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions api/v1alpha1/authorizationconfiguration_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// AuthorizationConfiguration provides versioned configuration for authorization.
type AuthorizationConfiguration struct {
metav1.TypeMeta
Type string `json:"type"`
CEL *CELConfiguration `json:"cel,omitempty"`
}

type CELConfiguration struct {
Expression string `json:"expression"`
}

func init() {
SchemeBuilder.Register(&AuthorizationConfiguration{})
}
44 changes: 44 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook"

jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1"
"github.com/jumpstarter-dev/jumpstarter-controller/internal/authentication"
"github.com/jumpstarter-dev/jumpstarter-controller/internal/authorization"
"github.com/jumpstarter-dev/jumpstarter-controller/internal/config"
"github.com/jumpstarter-dev/jumpstarter-controller/internal/controller"
Expand Down Expand Up @@ -152,7 +151,7 @@ func main() {
os.Exit(1)
}

authenticator, err := config.LoadConfiguration(
authn, authz, err := config.LoadConfiguration(
context.Background(),
mgr.GetAPIReader(),
mgr.GetScheme(),
Expand Down Expand Up @@ -205,8 +204,8 @@ func main() {
if err = (&service.ControllerService{
Client: watchClient,
Scheme: mgr.GetScheme(),
Authn: authentication.NewBearerTokenAuthenticator(authenticator),
Authz: authorization.NewBasicAuthorizer(watchClient, oidcSigner.Prefix()),
Authn: authn,
Authz: authz,
Attr: authorization.NewMetadataAttributesGetter(authorization.MetadataAttributesGetterConfig{
NamespaceKey: "jumpstarter-namespace",
ResourceKey: "jumpstarter-kind",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ metadata:
{{ end }}
data:
authentication: {{- .Values.authenticationConfig | toYaml | indent 1 }}
authorization: {{- .Values.authorizationConfig | toYaml | indent 1 }}
7 changes: 7 additions & 0 deletions deploy/helm/jumpstarter/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ jumpstarter-controller:
# claim: "sub"
# prefix: ""

authorizationConfig: |
apiVersion: jumpstarter.dev/v1alpha1
kind: AuthorizationConfiguration
type: CEL
cel:
expression: "self.spec.username == user.username"

grpc:
hostname: ""
routerHostname: ""
Expand Down
128 changes: 128 additions & 0 deletions internal/authorization/cel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package authorization

import (
"context"
"fmt"

celgo "github.com/google/cel-go/cel"
jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/pkg/cel/environment"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type CELAuthorizer struct {
reader client.Reader
prefix string
program celgo.Program
}
type Expression struct {
Expression string
}

func (v *Expression) GetExpression() string {
return v.Expression
}

func (v *Expression) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}

func NewCELAuthorizer(reader client.Reader, prefix string, expression string) (authorizer.Authorizer, error) {
env, err := environment.MustBaseEnvSet(
environment.DefaultCompatibilityVersion(),
false,
).Extend(environment.VersionedOptions{
IntroducedVersion: environment.DefaultCompatibilityVersion(),
EnvOptions: []celgo.EnvOption{
celgo.Variable("kind", celgo.StringType),
celgo.Variable("self", celgo.DynType),
celgo.Variable("user", celgo.DynType),
},
})
if err != nil {
return nil, err
}

compiler := cel.NewCompiler(env)

compiled, err := compiler.CompileCELExpression(&Expression{
Expression: expression,
})
if err != nil {
return nil, err
}

return &CELAuthorizer{
reader: reader,
prefix: prefix,
program: compiled.Program,
}, nil
}

func (b *CELAuthorizer) Authorize(
ctx context.Context,
attributes authorizer.Attributes,
) (authorizer.Decision, string, error) {
var self map[string]interface{}
var err error

Comment on lines +65 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Authorize() method handles resource fetching and CEL evaluation logically.

  1. The logic for retrieving Exporter or Client is correct, but consider gracefully handling the case where spec doesn't exist before using .(map[string]any), to avoid potential runtime panics.
  2. The CEL evaluation and final decision returning DecisionAllow or DecisionDeny is consistent with standard authorizer patterns.

Apply a safer approach for type assertions on self["spec"] to prevent panics, for example:

- self["spec"].(map[string]any)["username"] = e.Username(b.prefix)
+ spec, ok := self["spec"].(map[string]any)
+ if !ok {
+   return authorizer.DecisionDeny, "missing spec in resource", fmt.Errorf("spec field not found or invalid")
+ }
+ spec["username"] = e.Username(b.prefix)

Also applies to: 72-85, 87-99, 100-101, 103-116, 118-127

switch attributes.GetResource() {
case "Exporter":
var e jumpstarterdevv1alpha1.Exporter
if err := b.reader.Get(ctx, client.ObjectKey{
Namespace: attributes.GetNamespace(),
Name: attributes.GetName(),
}, &e); err != nil {
return authorizer.DecisionDeny, "failed to get exporter", err
}
self, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&e)
if err != nil {
return authorizer.DecisionDeny, "failed to serialize exporter", err
}
self["spec"].(map[string]any)["username"] = e.Username(b.prefix)
case "Client":
var c jumpstarterdevv1alpha1.Client
if err := b.reader.Get(ctx, client.ObjectKey{
Namespace: attributes.GetNamespace(),
Name: attributes.GetName(),
}, &c); err != nil {
return authorizer.DecisionDeny, "failed to get client", err
}
self, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&c)
if err != nil {
return authorizer.DecisionDeny, "failed to serialize client", err
}
self["spec"].(map[string]any)["username"] = c.Username(b.prefix)
default:
return authorizer.DecisionDeny, "invalid object kind", nil
}

user := attributes.GetUser()
value, _, err := b.program.Eval(map[string]any{
"kind": attributes.GetResource(),
"self": self,
"user": map[string]any{
"username": user.GetName(),
"uid": user.GetUID(),
"groups": user.GetGroups(),
"extra": user.GetExtra(),
},
})
if err != nil {
return authorizer.DecisionDeny, "failed to evaluate expression", err
}

result, ok := value.Value().(bool)
if !ok {
return authorizer.DecisionDeny, "failed to evaluate expression", fmt.Errorf("result type mismatch")
}

if result {
return authorizer.DecisionAllow, "", nil
} else {
return authorizer.DecisionDeny, "permission denied", nil
}
}
42 changes: 42 additions & 0 deletions internal/authorization/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package authorization

import (
"context"
"fmt"

jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func LoadAuthorizationConfiguration(
ctx context.Context,
scheme *runtime.Scheme,
configuration []byte,
reader client.Reader,
prefix string,
) (authorizer.Authorizer, error) {
var authorizationConfiguration jumpstarterdevv1alpha1.AuthorizationConfiguration
if err := runtime.DecodeInto(
serializer.NewCodecFactory(scheme, serializer.EnableStrict).
UniversalDecoder(jumpstarterdevv1alpha1.GroupVersion),
configuration,
&authorizationConfiguration,
); err != nil {
return nil, err
}

switch authorizationConfiguration.Type {
case "Basic":
return NewBasicAuthorizer(reader, prefix), nil
case "CEL":
if authorizationConfiguration.CEL == nil {
return nil, fmt.Errorf("CEL authorizer configuration missing")
}
return NewCELAuthorizer(reader, prefix, authorizationConfiguration.CEL.Expression)
default:
return nil, fmt.Errorf("unsupported authorizer type: %s", authorizationConfiguration.Type)
}
}
Comment on lines +31 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify CEL expression validity during initialization.

For the CEL authorizer type, it would be beneficial to validate the expression syntax during initialization to fail fast on invalid expressions.


🏁 Script executed:

#!/bin/bash
# Description: Search for CEL expression validation in the codebase.

# Test: Look for CEL expression validation in NewCELAuthorizer.
ast-grep --pattern $'func NewCELAuthorizer($_, $_, $expr string) ($_, error) {
  $$$
}'

# Test: Look for CEL expression validation in cel.go.
rg -A 5 'NewCELAuthorizer'

Length of output: 966


CEL expression validation: Additional check required during initialization

The current implementation calls NewCELAuthorizer with the provided CEL expression without performing any syntax checks. The verification confirms that neither the caller in internal/authorization/config.go nor the implementation in internal/authorization/cel.go includes an explicit validation step for the expression. To improve reliability and fail fast on misconfigurations, please add a validation routine (for example, leveraging the CEL parser) within NewCELAuthorizer before extending the environment.

  • File: internal/authorization/cel.go
    • Suggestion: Add a step to validate the expression parameter, ensuring it adheres to the expected CEL syntax.
  • File: internal/authorization/config.go
    • Observation: The switch case for the "CEL" type currently only checks for missing configuration (authorizationConfiguration.CEL == nil).

32 changes: 25 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"fmt"

"github.com/jumpstarter-dev/jumpstarter-controller/internal/authentication"
"github.com/jumpstarter-dev/jumpstarter-controller/internal/authorization"
"github.com/jumpstarter-dev/jumpstarter-controller/internal/oidc"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authorization/authorizer"
"sigs.k8s.io/controller-runtime/pkg/client"
)

Expand All @@ -18,27 +20,43 @@ func LoadConfiguration(
key client.ObjectKey,
signer *oidc.Signer,
certificateAuthority string,
) (authenticator.Token, error) {
) (authentication.ContextAuthenticator, authorizer.Authorizer, error) {
var configmap corev1.ConfigMap
if err := client.Get(ctx, key, &configmap); err != nil {
return nil, err
return nil, nil, err
}

rawAuthenticationConfiguration, ok := configmap.Data["authentication"]
if !ok {
return nil, fmt.Errorf("LoadConfiguration: missing authentication section")
return nil, nil, fmt.Errorf("LoadConfiguration: missing authentication section")
}

authenticator, err := oidc.LoadAuthenticationConfiguration(
authn, err := oidc.LoadAuthenticationConfiguration(
ctx,
scheme,
[]byte(rawAuthenticationConfiguration),
signer,
certificateAuthority,
)
if err != nil {
return nil, err
return nil, nil, err
}

return authenticator, nil
rawAuthorizationConfiguration, ok := configmap.Data["authorization"]
if !ok {
return nil, nil, fmt.Errorf("LoadConfiguration: missing authorization section")
}

authz, err := authorization.LoadAuthorizationConfiguration(
ctx,
scheme,
[]byte(rawAuthorizationConfiguration),
client,
signer.Prefix(),
)
if err != nil {
return nil, nil, err
}

return authentication.NewBearerTokenAuthenticator(authn), authz, nil
}