-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(backend): implement general api and service logic (#22)
* add type and pseudocode Signed-off-by: Alexandre Gaudreault <[email protected]> * dump Signed-off-by: Alexandre Gaudreault <[email protected]> * create logic Signed-off-by: Alexandre Gaudreault <[email protected]> * create api call Signed-off-by: Alexandre Gaudreault <[email protected]> * extract validation to service Signed-off-by: Alexandre Gaudreault <[email protected]> * implement API logic Signed-off-by: Alexandre Gaudreault <[email protected]> * use query params Signed-off-by: Alexandre Gaudreault <[email protected]> * query meant list. use subpath Signed-off-by: Alexandre Gaudreault <[email protected]> * fix struct ptr Signed-off-by: Alexandre Gaudreault <[email protected]> * cleanup api Signed-off-by: Alexandre Gaudreault <[email protected]> * fix build errors Signed-off-by: Alexandre Gaudreault <[email protected]> * log all 500 errors Signed-off-by: Alexandre Gaudreault <[email protected]> * fix existing tests Signed-off-by: Alexandre Gaudreault <[email protected]> * dist Signed-off-by: Alexandre Gaudreault <[email protected]> * return api object Signed-off-by: Alexandre Gaudreault <[email protected]> * index field Signed-off-by: Alexandre Gaudreault <[email protected]> * implement backend api unit tests Signed-off-by: Alexandre Gaudreault <[email protected]> * service setup Signed-off-by: Alexandre Gaudreault <[email protected]> * impl mocks for service test Signed-off-by: Alexandre Gaudreault <[email protected]> * get access request by role Signed-off-by: Alexandre Gaudreault <[email protected]> * add service test cases Signed-off-by: Alexandre Gaudreault <[email protected]> * set rolename in status Signed-off-by: Alexandre Gaudreault <[email protected]> * index ab Signed-off-by: Alexandre Gaudreault <[email protected]> * sort unit test Signed-off-by: Alexandre Gaudreault <[email protected]> * persister tests Signed-off-by: Alexandre Gaudreault <[email protected]> * safe indexing Signed-off-by: Alexandre Gaudreault <[email protected]> * fix eprsister test on cache delay Signed-off-by: Alexandre Gaudreault <[email protected]> * service tests Signed-off-by: Alexandre Gaudreault <[email protected]> * fix merge Signed-off-by: Alexandre Gaudreault <[email protected]> * add invalid status Signed-off-by: Alexandre Gaudreault <[email protected]> * test template rendering Signed-off-by: Alexandre Gaudreault <[email protected]> * removed unused get api call Signed-off-by: Alexandre Gaudreault <[email protected]> * code review changes Signed-off-by: Alexandre Gaudreault <[email protected]> * self review Signed-off-by: Alexandre Gaudreault <[email protected]> --------- Signed-off-by: Alexandre Gaudreault <[email protected]>
- Loading branch information
1 parent
e85a9fb
commit a65f03f
Showing
26 changed files
with
3,141 additions
and
275 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* | ||
Copyright 2024. | ||
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 v1alpha1 | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"text/template" | ||
|
||
"github.com/expr-lang/expr" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
) | ||
|
||
// AccessBinding is the Schema for the accessbindings API | ||
// +kubebuilder:object:root=true | ||
type AccessBinding struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ObjectMeta `json:"metadata,omitempty"` | ||
|
||
Spec AccessBindingSpec `json:"spec,omitempty"` | ||
} | ||
|
||
// AccessBindingList contains a list of AccessBinding | ||
// +kubebuilder:object:root=true | ||
type AccessBindingList struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ListMeta `json:"metadata,omitempty"` | ||
|
||
Items []AccessBinding `json:"items"` | ||
} | ||
|
||
// AccessBindingSpec defines the desired state of AccessBinding | ||
type AccessBindingSpec struct { | ||
// RoleTemplateRef is the reference to the RoleTemplate this bindings grants access to | ||
// +kubebuilder:validation:Required | ||
RoleTemplateRef RoleTemplateReference `json:"roleTemplateRef"` | ||
// Subjects is list of strings, supporting go template, that a user's group claims must match at least one of to be allowed | ||
Subjects []string `json:"subjects"` | ||
// If is a condition that must be true to evaluate the subjects | ||
If *string `json:"if,omitempty"` | ||
// Ordinal defines an ordering number of this role compared to others | ||
Ordinal int `json:"ordinal,omitempty"` | ||
// FriendlyName defines a name for this role | ||
// +kubebuilder:validation:MaxLength=512 | ||
FriendlyName *string `json:"friendlyName,omitempty"` | ||
} | ||
|
||
// RoleTemplateReference is a reference to a RoleTemplate | ||
type RoleTemplateReference struct { | ||
// Name of the role template object | ||
// +kubebuilder:validation:Required | ||
Name string `json:"name"` | ||
} | ||
|
||
// RenderSubjects renders the access bindings subjects when the If condition is evaluated to true | ||
func (ab *AccessBinding) RenderSubjects(app, project *unstructured.Unstructured) ([]string, error) { | ||
if len(ab.Spec.Subjects) == 0 { | ||
return nil, nil | ||
} | ||
|
||
values := map[string]interface{}{ | ||
"app": app.Object, | ||
"application": app.Object, | ||
"project": project.Object, | ||
} | ||
|
||
if ab.Spec.If != nil { | ||
out, err := expr.Eval(*ab.Spec.If, values) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to evaluate binding condition '%s': %w", *ab.Spec.If, err) | ||
} | ||
switch condResult := out.(type) { | ||
case bool: | ||
if !condResult { | ||
// No need to render template, condition is false | ||
return nil, nil | ||
} | ||
default: | ||
return nil, fmt.Errorf("binding condition '%s' evaluated to non-boolean value", *ab.Spec.If) | ||
} | ||
} | ||
|
||
subStr := strings.Join(ab.Spec.Subjects, "\n") | ||
subTmpl, err := template.New("subjects").Parse(subStr) | ||
if err != nil { | ||
return nil, fmt.Errorf("error parsing AccessBinding subjects: %w", err) | ||
} | ||
p, err := ab.execTemplate(subTmpl, values) | ||
if err != nil { | ||
return nil, fmt.Errorf("error rendering AccessBinding subjects: %w", err) | ||
} | ||
subjects := strings.Split(p, "\n") | ||
|
||
return subjects, nil | ||
} | ||
|
||
func (ab *AccessBinding) execTemplate( | ||
tmpl *template.Template, | ||
values any, | ||
) (string, error) { | ||
var s strings.Builder | ||
err := tmpl.Execute(&s, values) | ||
if err != nil { | ||
return "", err | ||
} | ||
return s.String(), nil | ||
} | ||
|
||
func init() { | ||
SchemeBuilder.Register(&AccessBinding{}, &AccessBindingList{}) | ||
} |
117 changes: 117 additions & 0 deletions
117
api/ephemeral-access/v1alpha1/accessbinding_types_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package v1alpha1_test | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1" | ||
api "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1" | ||
"github.com/argoproj-labs/ephemeral-access/test/utils" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/utils/ptr" | ||
) | ||
|
||
func TestAccessBinding_RenderSubjects(t *testing.T) { | ||
app, err := utils.ToUnstructured(&v1alpha1.Application{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Name: "test", | ||
Annotations: map[string]string{ | ||
"test": "hello", | ||
}, | ||
}, | ||
}) | ||
require.NoError(t, err) | ||
project, err := utils.ToUnstructured(&v1alpha1.AppProject{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Name: "test", | ||
Annotations: map[string]string{ | ||
"test": "world", | ||
}, | ||
}, | ||
}) | ||
require.NoError(t, err) | ||
|
||
tests := []struct { | ||
name string | ||
If *string | ||
subjects []string | ||
expected []string | ||
errorContains string | ||
}{ | ||
{ | ||
name: "sucessfully return rendered subjects", | ||
subjects: []string{ | ||
`{{ index .application.metadata.annotations "test" }}`, | ||
`{{ index .project.metadata.annotations "test" }}`, | ||
}, | ||
expected: []string{"hello", "world"}, | ||
}, | ||
{ | ||
name: "return nil if subjects are nil", | ||
subjects: nil, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "return nil if subjects are empty", | ||
subjects: []string{}, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "return nil if If condition is false", | ||
If: ptr.To("false"), | ||
subjects: []string{"value"}, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "return error if If condition is invalid", | ||
If: ptr.To("invalid.golang"), | ||
subjects: []string{"value"}, | ||
errorContains: "failed to evaluate binding condition", | ||
}, | ||
{ | ||
name: "return error if If condition is not a boolean", | ||
If: ptr.To("1 + 1"), | ||
subjects: []string{"value"}, | ||
errorContains: "evaluated to non-boolean value", | ||
}, | ||
{ | ||
name: "return error if subjects template is invalid", | ||
subjects: []string{"{{"}, | ||
errorContains: "error parsing AccessBinding subjects", | ||
}, | ||
{ | ||
name: "return error if subjects template rendering is invalid", | ||
subjects: []string{`{{ index .notAnObject "key" }}`}, | ||
errorContains: "error rendering AccessBinding subjects", | ||
}, | ||
{ | ||
name: "can render app as an alias", | ||
subjects: []string{"{{ .app.metadata.name }}"}, | ||
expected: []string{"test"}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
ab := &api.AccessBinding{ | ||
Spec: api.AccessBindingSpec{ | ||
If: tt.If, | ||
Subjects: tt.subjects, | ||
}, | ||
} | ||
got, err := ab.RenderSubjects(app, project) | ||
if err != nil { | ||
if tt.errorContains != "" { | ||
assert.ErrorContains(t, err, tt.errorContains) | ||
} else { | ||
assert.NoError(t, err) | ||
} | ||
return | ||
} | ||
if !reflect.DeepEqual(got, tt.expected) { | ||
t.Errorf("AccessBinding.RenderSubjects() = %v, want %v", got, tt.expected) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.