-
Notifications
You must be signed in to change notification settings - Fork 8
Customizable client authorization #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
97b0be4
9fed5a8
b153a1a
857ff4a
0c9f009
6fafded
ce035dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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{}) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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 | ||
|
||
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 | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify 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
|
There was a problem hiding this comment.
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.Exporter
orClient
is correct, but consider gracefully handling the case wherespec
doesn't exist before using.(map[string]any)
, to avoid potential runtime panics.DecisionAllow
orDecisionDeny
is consistent with standard authorizer patterns.Apply a safer approach for type assertions on
self["spec"]
to prevent panics, for example:Also applies to: 72-85, 87-99, 100-101, 103-116, 118-127