diff --git a/api/v1beta3/auth_config_types.go b/api/v1beta3/auth_config_types.go index ec162ffa..b6274d13 100644 --- a/api/v1beta3/auth_config_types.go +++ b/api/v1beta3/auth_config_types.go @@ -622,6 +622,9 @@ type KubernetesSubjectAccessReviewAuthorizationSpec struct { // Groups the user must be a member of or, if `user` is omitted, the groups to check for authorization in the Kubernetes RBAC. Groups []string `json:"groups,omitempty"` + // AuthorizationGroups is a value or selector to use as groups to check for authorization in the Kubernetes RBAC. + AuthorizationGroups *ValueOrSelector `json:"authorizationGroups,omitempty"` + // Use resourceAttributes to check permissions on Kubernetes resources. // If omitted, it performs a non-resource SubjectAccessReview, with verb and path inferred from the request. // +optional diff --git a/api/v1beta3/zz_generated.deepcopy.go b/api/v1beta3/zz_generated.deepcopy.go index cbc7f560..382a3ad7 100644 --- a/api/v1beta3/zz_generated.deepcopy.go +++ b/api/v1beta3/zz_generated.deepcopy.go @@ -779,6 +779,11 @@ func (in *KubernetesSubjectAccessReviewAuthorizationSpec) DeepCopyInto(out *Kube *out = make([]string, len(*in)) copy(*out, *in) } + if in.AuthorizationGroups != nil { + in, out := &in.AuthorizationGroups, &out.AuthorizationGroups + *out = new(ValueOrSelector) + (*in).DeepCopyInto(*out) + } if in.ResourceAttributes != nil { in, out := &in.ResourceAttributes, &out.ResourceAttributes *out = new(KubernetesSubjectAccessReviewResourceAttributesSpec) diff --git a/controllers/auth_config_controller.go b/controllers/auth_config_controller.go index 5dbce879..e54fdac0 100644 --- a/controllers/auth_config_controller.go +++ b/controllers/auth_config_controller.go @@ -503,7 +503,14 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf } } - translatedAuthorization.KubernetesAuthz, err = authorization_evaluators.NewKubernetesAuthz(authorinoUser, authorization.KubernetesSubjectAccessReview.Groups, authorinoResourceAttributes) + var authorinoGroups expressions.Value + if authorization.KubernetesSubjectAccessReview.AuthorizationGroups != nil { + authorinoGroups, err = valueFrom(authorization.KubernetesSubjectAccessReview.AuthorizationGroups) + if err != nil { + return nil, err + } + } + translatedAuthorization.KubernetesAuthz, err = authorization_evaluators.NewKubernetesAuthz(authorinoUser, authorization.KubernetesSubjectAccessReview.Groups, authorinoGroups, authorinoResourceAttributes) if err != nil { return nil, err } diff --git a/install/crd/authorino.kuadrant.io_authconfigs.yaml b/install/crd/authorino.kuadrant.io_authconfigs.yaml index 242ba008..df0b3ac7 100644 --- a/install/crd/authorino.kuadrant.io_authconfigs.yaml +++ b/install/crd/authorino.kuadrant.io_authconfigs.yaml @@ -2796,6 +2796,23 @@ spec: kubernetesSubjectAccessReview: description: Authorization by Kubernetes SubjectAccessReview properties: + authorizationGroups: + description: AuthorizationGroups is a value or selector + to use as groups to check for authorization in the Kubernetes + RBAC. + properties: + expression: + type: string + selector: + description: |- + Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. + The following Authorino custom modifiers are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, @base64:encode|decode and @strip. + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object groups: description: Groups the user must be a member of or, if `user` is omitted, the groups to check for authorization diff --git a/install/manifests.yaml b/install/manifests.yaml index 46761613..7e0e7807 100644 --- a/install/manifests.yaml +++ b/install/manifests.yaml @@ -3104,6 +3104,23 @@ spec: kubernetesSubjectAccessReview: description: Authorization by Kubernetes SubjectAccessReview properties: + authorizationGroups: + description: AuthorizationGroups is a value or selector + to use as groups to check for authorization in the Kubernetes + RBAC. + properties: + expression: + type: string + selector: + description: |- + Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. + The following Authorino custom modifiers are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, @base64:encode|decode and @strip. + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object groups: description: Groups the user must be a member of or, if `user` is omitted, the groups to check for authorization diff --git a/pkg/evaluators/authorization/kubernetes_authz.go b/pkg/evaluators/authorization/kubernetes_authz.go index de398938..72d81922 100644 --- a/pkg/evaluators/authorization/kubernetes_authz.go +++ b/pkg/evaluators/authorization/kubernetes_authz.go @@ -2,6 +2,7 @@ package authorization import ( gocontext "context" + gojson "encoding/json" "fmt" "strings" @@ -22,7 +23,7 @@ type kubernetesSubjectAccessReviewer interface { SubjectAccessReviews() kubeAuthzClient.SubjectAccessReviewInterface } -func NewKubernetesAuthz(user expressions.Value, groups []string, resourceAttributes *KubernetesAuthzResourceAttributes) (*KubernetesAuthz, error) { +func NewKubernetesAuthz(user expressions.Value, groups []string, authorizationGroups expressions.Value, resourceAttributes *KubernetesAuthzResourceAttributes) (*KubernetesAuthz, error) { config, err := rest.InClusterConfig() if err != nil { return nil, err @@ -34,10 +35,11 @@ func NewKubernetesAuthz(user expressions.Value, groups []string, resourceAttribu } return &KubernetesAuthz{ - User: user, - Groups: groups, - ResourceAttributes: resourceAttributes, - authorizer: k8sClient.AuthorizationV1(), + User: user, + Groups: groups, + AuthorizationGroups: authorizationGroups, + ResourceAttributes: resourceAttributes, + authorizer: k8sClient.AuthorizationV1(), }, nil } @@ -51,9 +53,10 @@ type KubernetesAuthzResourceAttributes struct { } type KubernetesAuthz struct { - User expressions.Value - Groups []string - ResourceAttributes *KubernetesAuthzResourceAttributes + User expressions.Value + Groups []string + AuthorizationGroups expressions.Value + ResourceAttributes *KubernetesAuthzResourceAttributes authorizer kubernetesSubjectAccessReviewer } @@ -131,6 +134,21 @@ func (k *KubernetesAuthz) Call(pipeline auth.AuthPipeline, ctx gocontext.Context if len(k.Groups) > 0 { subjectAccessReview.Spec.Groups = k.Groups + } else if k.AuthorizationGroups != nil { + resolvedValue, err := k.AuthorizationGroups.ResolveFor(authJSON) + if err != nil { + return nil, err + } + stringJson, err := json.StringifyJSON(resolvedValue) + if err != nil { + return nil, err + } + var resolvedGroups []string + err = gojson.Unmarshal([]byte(stringJson), &resolvedGroups) + if err != nil { + return nil, err + } + subjectAccessReview.Spec.Groups = resolvedGroups } log.FromContext(ctx).WithName("kubernetesauthz").V(1).Info("calling kubernetes subject access review api", "subjectaccessreview", subjectAccessReview) diff --git a/pkg/evaluators/authorization/kubernetes_authz_test.go b/pkg/evaluators/authorization/kubernetes_authz_test.go index 77a54826..fa18f81b 100644 --- a/pkg/evaluators/authorization/kubernetes_authz_test.go +++ b/pkg/evaluators/authorization/kubernetes_authz_test.go @@ -59,11 +59,12 @@ func (client *k8sAuthorizationClientMock) GetRequest() kubeAuthz.SubjectAccessRe return client.request } -func newKubernetesAuthz(user expressions.Value, groups []string, resourceAttributes *KubernetesAuthzResourceAttributes, subjectAccessReviewResponseStatus kubeAuthz.SubjectAccessReviewStatus) *KubernetesAuthz { +func newKubernetesAuthz(user expressions.Value, groups []string, authorizationGroups expressions.Value, resourceAttributes *KubernetesAuthzResourceAttributes, subjectAccessReviewResponseStatus kubeAuthz.SubjectAccessReviewStatus) *KubernetesAuthz { return &KubernetesAuthz{ - User: user, - Groups: groups, - ResourceAttributes: resourceAttributes, + User: user, + Groups: groups, + AuthorizationGroups: authorizationGroups, + ResourceAttributes: resourceAttributes, // mock the authorizer so we can control the response authorizer: &k8sAuthorizationClientMock{SubjectAccessReviewStatus: subjectAccessReviewResponseStatus}, @@ -75,7 +76,7 @@ func TestKubernetesAuthzNonResource_Allowed(t *testing.T) { defer ctrl.Finish() pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) - pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"method":"GET","path":"/hello"}}},"auth":{"identity":{"username":"john"}}}`) + pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"method":"GET","path":"/hello"}}},"auth":{"identity":{"username":"john", "groups":["group1","group2"]}}}`) request := &envoy_auth.AttributeContext_HttpRequest{Method: "GET", Path: "/hello"} pipelineMock.EXPECT().GetHttp().Return(request) @@ -83,6 +84,7 @@ func TestKubernetesAuthzNonResource_Allowed(t *testing.T) { kubernetesAuth := newKubernetesAuthz( &json.JSONValue{Pattern: "auth.identity.username"}, []string{}, + &json.JSONValue{Pattern: "auth.identity.groups"}, nil, kubeAuthz.SubjectAccessReviewStatus{Allowed: true, Reason: ""}, ) @@ -112,6 +114,7 @@ func TestKubernetesAuthzNonResource_Denied(t *testing.T) { &json.JSONValue{Pattern: "auth.identity.username"}, []string{}, nil, + nil, kubeAuthz.SubjectAccessReviewStatus{Allowed: false, Reason: "some-reason"}, ) authorized, err := kubernetesAuth.Call(pipelineMock, context.TODO()) @@ -131,11 +134,12 @@ func TestKubernetesAuthzResource_Allowed(t *testing.T) { defer ctrl.Finish() pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) - pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"method":"GET","path":"/hello"}}},"auth":{"identity":{"username":"john"}}}`) + pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"method":"GET","path":"/hello"}}},"auth":{"identity":{"username":"john", "groups":["group1","group2"]}}}`) kubernetesAuth := newKubernetesAuthz( &json.JSONValue{Pattern: "auth.identity.username"}, []string{}, + &json.JSONValue{Pattern: "auth.identity.groups"}, &KubernetesAuthzResourceAttributes{Namespace: &json.JSONValue{Static: "default"}}, kubeAuthz.SubjectAccessReviewStatus{Allowed: true, Reason: ""}, ) @@ -160,6 +164,7 @@ func TestKubernetesAuthzResource_Denied(t *testing.T) { kubernetesAuth := newKubernetesAuthz( &json.JSONValue{Pattern: "auth.identity.username"}, []string{}, + nil, &KubernetesAuthzResourceAttributes{Namespace: &json.JSONValue{Static: "default"}}, kubeAuthz.SubjectAccessReviewStatus{Allowed: false, Reason: "some-reason"}, )