From e3f16c51ef6105349c036714062931c1aafbc24e Mon Sep 17 00:00:00 2001 From: FrancescoL96 Date: Mon, 6 Oct 2025 16:11:34 +0200 Subject: [PATCH 1/6] refactor: improved restaction flow to use parameters --- internal/helpers/restaction/resolver.go | 110 ++------------------ internal/helpers/restaction/restactions.go | 113 --------------------- 2 files changed, 7 insertions(+), 216 deletions(-) delete mode 100644 internal/helpers/restaction/restactions.go diff --git a/internal/helpers/restaction/resolver.go b/internal/helpers/restaction/resolver.go index a6b8095..faae36a 100644 --- a/internal/helpers/restaction/resolver.go +++ b/internal/helpers/restaction/resolver.go @@ -8,11 +8,7 @@ import ( "net/http" "github.com/krateoplatformops/authn/apis/core" - "github.com/krateoplatformops/authn/internal/helpers/kube/secrets" xcontext "github.com/krateoplatformops/plumbing/context" - "github.com/krateoplatformops/plumbing/kubeutil" - templatesv1 "github.com/krateoplatformops/snowplow/apis/templates/v1" - v1 "k8s.io/api/core/v1" "k8s.io/client-go/rest" ) @@ -23,16 +19,15 @@ type Response struct { } func Resolve(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, email string, bearerToken string) (map[string]interface{}, error) { - restactionCopy, err := copyRestActionWithEndpoints(ctx, rc, restaction, email, bearerToken) - if err != nil { - return nil, fmt.Errorf("could not resolve restaction: %v", err) - } - + jsonToken, _ := json.Marshal(map[string]string{ + "token": bearerToken, + }) // Call the RESTAction from snowplow url := fmt.Sprintf(ctx.Value(RestActionContextKey("snowplowURL")).(string)+ - "/call?apiVersion=templates.krateo.io%%2Fv1&resource=restactions&name=%s&namespace=%s", - restactionCopy.Name, - restactionCopy.Namespace) + "/call?apiVersion=templates.krateo.io%%2Fv1&resource=restactions&name=%s&namespace=%s&extras=%s", + restaction.Name, + restaction.Namespace, + jsonToken) request, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create http request for restaction call to snowplow: %w", err) @@ -57,96 +52,5 @@ func Resolve(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, e return nil, fmt.Errorf("error parsing userinfo payload JSON: %v\npayload: %s", err, string(responseRaw)) } - err = deleteRestActionCopyWithEndpoints(ctx, rc, restactionCopy) - if err != nil { - return nil, fmt.Errorf("error while deleting copy of restaction and secrets: %v", err) - } - return responseStruct.Status, nil } - -// Make a copy of the restAction object for the current user, as well as all endpoints -func copyRestActionWithEndpoints(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, email string, bearerToken string) (*core.ObjectRef, error) { - restactionObj, err := Get(ctx, rc, restaction) - if err != nil { - return nil, fmt.Errorf("could not obtain restaction object for %s %s: %w", restaction.Name, restaction.Namespace, err) - } - - restactionObjCopy := &templatesv1.RESTAction{} - restactionObjCopy.Name = restactionObj.Name + "-" + kubeutil.MakeDNS1123Compatible(email) - restactionObjCopy.Namespace = restactionObj.Namespace - restactionObjCopy.Spec = restactionObj.Spec - - for i, api := range restactionObjCopy.Spec.API { - if api.EndpointRef != nil { - secretSelector := &core.SecretKeySelector{Name: api.EndpointRef.Name, Namespace: api.EndpointRef.Namespace} - restactionSecret, err := secrets.Get(ctx, rc, secretSelector) - if err != nil { - return nil, fmt.Errorf("could not read endpoint secret object for restaction %s %s, endpoint %s %s: %w", restaction.Name, restaction.Namespace, secretSelector.Name, secretSelector.Namespace, err) - } - - restactionSecretCopy := &v1.Secret{} - restactionSecretCopy.Name = restactionSecret.Name + "-" + kubeutil.MakeDNS1123Compatible(email) - restactionSecretCopy.Namespace = restactionSecret.Namespace - restactionSecretCopy.Data = restactionSecret.Data - if _, ok := restactionSecretCopy.Data["token"]; !ok { - restactionSecretCopy.StringData = make(map[string]string) - restactionSecretCopy.StringData["token"] = bearerToken - } - - restactionObjCopy.Spec.API[i].EndpointRef.Name = restactionSecretCopy.Name - err = secrets.CreateOrUpdate(ctx, rc, restactionSecretCopy) - if err != nil { - return nil, fmt.Errorf("could not create copy of endpoint secret object for restaction %s (%s) %s, endpoint %s %s: %w", restaction.Name, restaction.Namespace, secretSelector.Name, restactionSecret.Name, secretSelector.Namespace, err) - } - } - } - - err = CreateOrUpdate(ctx, rc, restactionObjCopy) - if err != nil { - return nil, fmt.Errorf("could not create copy of restaction object for %s (%s) %s: %w", restaction.Name, restactionObjCopy.Name, restaction.Namespace, err) - } - - return &core.ObjectRef{Name: restactionObjCopy.Name, Namespace: restactionObjCopy.Namespace}, nil -} - -// Delete the copy of the restAction object for the current user, as well as all endpoints -func deleteRestActionCopyWithEndpoints(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef) error { - restactionObj, err := Get(ctx, rc, restaction) - if err != nil { - return fmt.Errorf("could not obtain restaction object to delete for %s %s: %w", restaction.Name, restaction.Namespace, err) - } - - // Delete all secret endpoints first - deleted := make([]*core.SecretKeySelector, 0) - for _, api := range restactionObj.Spec.API { - if api.EndpointRef != nil { - secretSelector := &core.SecretKeySelector{Name: api.EndpointRef.Name, Namespace: api.EndpointRef.Namespace} - if hasSecret(deleted, secretSelector) { // The endpoint copy has already been deleted - continue - } - err = secrets.Delete(ctx, rc, secretSelector) - if err != nil { - return fmt.Errorf("could not delete copy of endpoint secret object for restaction %s %s, endpoint %s %s: %w", restaction.Name, restaction.Namespace, secretSelector.Name, secretSelector.Namespace, err) - } - deleted = append(deleted, secretSelector) - } - } - - // Delete restaction copy - err = Delete(ctx, rc, restaction) - if err != nil { - return fmt.Errorf("could not delete copy of restaction object for %s %s: %w", restaction.Name, restaction.Namespace, err) - } - - return nil -} - -func hasSecret(list []*core.SecretKeySelector, obj *core.SecretKeySelector) bool { - for _, toCheck := range list { - if toCheck.Name == obj.Name && toCheck.Namespace == obj.Namespace { - return true - } - } - return false -} diff --git a/internal/helpers/restaction/restactions.go b/internal/helpers/restaction/restactions.go deleted file mode 100644 index 2bfe7c9..0000000 --- a/internal/helpers/restaction/restactions.go +++ /dev/null @@ -1,113 +0,0 @@ -package restaction - -import ( - "context" - - "github.com/krateoplatformops/authn/apis/core" - "github.com/krateoplatformops/authn/internal/helpers/kube/client" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/client-go/rest" - - snowplowapis "github.com/krateoplatformops/snowplow/apis" - snowplow "github.com/krateoplatformops/snowplow/apis/templates/v1" -) - -// Add this to your client package -func newWithScheme(config *rest.Config, gv schema.GroupVersion, scheme *runtime.Scheme) (*rest.RESTClient, error) { - // Clone your existing New function but add scheme support - config = rest.CopyConfig(config) - config.GroupVersion = &gv - config.APIPath = "/apis" - config.NegotiatedSerializer = serializer.NewCodecFactory(scheme) - config.UserAgent = rest.DefaultKubernetesUserAgent() - - return rest.RESTClientFor(config) -} - -func Get(ctx context.Context, rc *rest.Config, ref *core.ObjectRef) (*snowplow.RESTAction, error) { - cli, err := client.New(rc, schema.GroupVersion{Group: "templates.krateo.io", Version: "v1"}) - if err != nil { - return nil, err - } - - res := &snowplow.RESTAction{} - err = cli.Get(). - Resource("restactions"). - Namespace(ref.Namespace).Name(ref.Name). - Do(ctx). - Into(res) - - return res, err -} - -func CreateOrUpdate(ctx context.Context, rc *rest.Config, restaction *snowplow.RESTAction) error { - scheme := runtime.NewScheme() - snowplowapis.AddToScheme(scheme) - cli, err := newWithScheme(rc, schema.GroupVersion{Group: "templates.krateo.io", Version: "v1"}, scheme) - if err != nil { - return err - } - - // First try to get the resource - existingObj := &snowplow.RESTAction{} - err = cli.Get(). - Namespace(restaction.GetNamespace()). - Resource("restactions"). - Name(restaction.GetName()). - Do(ctx). - Into(existingObj) - - if err != nil { - if apierrors.IsNotFound(err) { - // Resource doesn't exist, create it - return cli.Post(). - Namespace(restaction.GetNamespace()). - Resource("restactions"). - Body(restaction). - Do(ctx). - Error() - } - return err // Return any other error - } - - restaction.ResourceVersion = existingObj.ResourceVersion - // Resource exists, update it - return cli.Put(). - Namespace(restaction.GetNamespace()). - Resource("restactions"). - Name(restaction.GetName()). - Body(restaction). - Do(ctx). - Error() -} - -func Update(ctx context.Context, rc *rest.Config, restaction *snowplow.RESTAction) error { - cli, err := client.New(rc, schema.GroupVersion{Group: "templates.krateo.io", Version: "v1"}) - if err != nil { - return err - } - return cli.Put(). - Namespace(restaction.GetNamespace()). - Resource("restactions"). - Name(restaction.Name). - Body(restaction). - Do(ctx). - Error() -} - -func Delete(ctx context.Context, rc *rest.Config, ref *core.ObjectRef) error { - cli, err := client.New(rc, schema.GroupVersion{Group: "templates.krateo.io", Version: "v1"}) - if err != nil { - return err - } - - return cli.Delete(). - Namespace(ref.Namespace). - Resource("restactions"). - Name(ref.Name). - Do(ctx). - Error() -} From 9502b751d4d143de421ad8d7f3c2b3e5b0df9c97 Mon Sep 17 00:00:00 2001 From: FrancescoL96 Date: Mon, 6 Oct 2025 16:11:56 +0200 Subject: [PATCH 2/6] docs: updated example restactions for parametrized calls --- testdata/oauth.yaml | 2 ++ testdata/oidc-azure.yaml | 1 + 2 files changed, 3 insertions(+) diff --git a/testdata/oauth.yaml b/testdata/oauth.yaml index cfc6d17..9ee334b 100644 --- a/testdata/oauth.yaml +++ b/testdata/oauth.yaml @@ -39,6 +39,7 @@ spec: headers: - 'Accept: application/vnd.github+json' - 'X-GitHub-Api-Version: 2022-11-28' + - "${ \"Authorization: Bearer \" + .token }" path: "/user" endpointRef: name: github-api @@ -49,6 +50,7 @@ spec: verb: POST headers: - 'Content-Type: application/json' + - "${ \"Authorization: Bearer \" + .token }" path: "" payload: | ${ {query:"query{organization(login:\"krateoplatformops\"){teams(first:100,userLogins:[\"" + .userInfo.name + "\"]){edges{node{slug}}}}}"} } diff --git a/testdata/oidc-azure.yaml b/testdata/oidc-azure.yaml index ab368a3..e28e736 100644 --- a/testdata/oidc-azure.yaml +++ b/testdata/oidc-azure.yaml @@ -53,6 +53,7 @@ spec: verb: GET headers: - 'Accept: application/json' + - "${ \"Authorization: Bearer \" + .token }" path: "v1.0/me/memberOf?$select=displayName" endpointRef: name: azure-entra From 45c660b17e2e95296f9b5746daea638b7b60b726 Mon Sep 17 00:00:00 2001 From: FrancescoL96 Date: Mon, 6 Oct 2025 17:42:26 +0200 Subject: [PATCH 3/6] feat: added fallback to legacy restaction resolver --- internal/helpers/restaction/resolver.go | 138 +++++++++++++++++++++ internal/helpers/restaction/restactions.go | 113 +++++++++++++++++ internal/routes/auth/oauth/login.go | 10 +- internal/routes/auth/oidc/login.go | 10 +- 4 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 internal/helpers/restaction/restactions.go diff --git a/internal/helpers/restaction/resolver.go b/internal/helpers/restaction/resolver.go index faae36a..ba0d508 100644 --- a/internal/helpers/restaction/resolver.go +++ b/internal/helpers/restaction/resolver.go @@ -8,7 +8,11 @@ import ( "net/http" "github.com/krateoplatformops/authn/apis/core" + "github.com/krateoplatformops/authn/internal/helpers/kube/secrets" xcontext "github.com/krateoplatformops/plumbing/context" + "github.com/krateoplatformops/plumbing/kubeutil" + templatesv1 "github.com/krateoplatformops/snowplow/apis/templates/v1" + v1 "k8s.io/api/core/v1" "k8s.io/client-go/rest" ) @@ -46,6 +50,11 @@ func Resolve(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, e responseRaw, _ := io.ReadAll(resp.Body) defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("snowplow endpoint returned a non-200 status code: %s", string(body)) + } + var responseStruct Response err = json.Unmarshal(responseRaw, &responseStruct) if err != nil { @@ -54,3 +63,132 @@ func Resolve(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, e return responseStruct.Status, nil } + +func LegacyResolve(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, email string, bearerToken string) (map[string]interface{}, error) { + restactionCopy, err := copyRestActionWithEndpoints(ctx, rc, restaction, email, bearerToken) + if err != nil { + return nil, fmt.Errorf("could not resolve restaction: %v", err) + } + + // Call the RESTAction from snowplow + url := fmt.Sprintf(ctx.Value(RestActionContextKey("snowplowURL")).(string)+ + "/call?apiVersion=templates.krateo.io%%2Fv1&resource=restactions&name=%s&namespace=%s", + restactionCopy.Name, + restactionCopy.Namespace) + request, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create http request for restaction call to snowplow: %w", err) + } + + jwt, ok := xcontext.AccessToken(ctx) + if !ok { + return nil, fmt.Errorf("failed to retrieve jwt token for authn") + } + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt)) + + resp, err := http.DefaultClient.Do(request) + if err != nil { + return nil, fmt.Errorf("failed to send http request for restaction call to snowplow: %w", err) + } + responseRaw, _ := io.ReadAll(resp.Body) + defer resp.Body.Close() + + var responseStruct Response + err = json.Unmarshal(responseRaw, &responseStruct) + if err != nil { + return nil, fmt.Errorf("error parsing userinfo payload JSON: %v\npayload: %s", err, string(responseRaw)) + } + + err = deleteRestActionCopyWithEndpoints(ctx, rc, restactionCopy) + if err != nil { + return nil, fmt.Errorf("error while deleting copy of restaction and secrets: %v", err) + } + + return responseStruct.Status, nil +} + +// Make a copy of the restAction object for the current user, as well as all endpoints +func copyRestActionWithEndpoints(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, email string, bearerToken string) (*core.ObjectRef, error) { + restactionObj, err := Get(ctx, rc, restaction) + if err != nil { + return nil, fmt.Errorf("could not obtain restaction object for %s %s: %w", restaction.Name, restaction.Namespace, err) + } + + restactionObjCopy := &templatesv1.RESTAction{} + restactionObjCopy.Name = restactionObj.Name + "-" + kubeutil.MakeDNS1123Compatible(email) + restactionObjCopy.Namespace = restactionObj.Namespace + restactionObjCopy.Spec = restactionObj.Spec + + for i, api := range restactionObjCopy.Spec.API { + if api.EndpointRef != nil { + secretSelector := &core.SecretKeySelector{Name: api.EndpointRef.Name, Namespace: api.EndpointRef.Namespace} + restactionSecret, err := secrets.Get(ctx, rc, secretSelector) + if err != nil { + return nil, fmt.Errorf("could not read endpoint secret object for restaction %s %s, endpoint %s %s: %w", restaction.Name, restaction.Namespace, secretSelector.Name, secretSelector.Namespace, err) + } + + restactionSecretCopy := &v1.Secret{} + restactionSecretCopy.Name = restactionSecret.Name + "-" + kubeutil.MakeDNS1123Compatible(email) + restactionSecretCopy.Namespace = restactionSecret.Namespace + restactionSecretCopy.Data = restactionSecret.Data + if _, ok := restactionSecretCopy.Data["token"]; !ok { + restactionSecretCopy.StringData = make(map[string]string) + restactionSecretCopy.StringData["token"] = bearerToken + } + + restactionObjCopy.Spec.API[i].EndpointRef.Name = restactionSecretCopy.Name + err = secrets.CreateOrUpdate(ctx, rc, restactionSecretCopy) + if err != nil { + return nil, fmt.Errorf("could not create copy of endpoint secret object for restaction %s (%s) %s, endpoint %s %s: %w", restaction.Name, restaction.Namespace, secretSelector.Name, restactionSecret.Name, secretSelector.Namespace, err) + } + } + } + + err = CreateOrUpdate(ctx, rc, restactionObjCopy) + if err != nil { + return nil, fmt.Errorf("could not create copy of restaction object for %s (%s) %s: %w", restaction.Name, restactionObjCopy.Name, restaction.Namespace, err) + } + + return &core.ObjectRef{Name: restactionObjCopy.Name, Namespace: restactionObjCopy.Namespace}, nil +} + +// Delete the copy of the restAction object for the current user, as well as all endpoints +func deleteRestActionCopyWithEndpoints(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef) error { + restactionObj, err := Get(ctx, rc, restaction) + if err != nil { + return fmt.Errorf("could not obtain restaction object to delete for %s %s: %w", restaction.Name, restaction.Namespace, err) + } + + // Delete all secret endpoints first + deleted := make([]*core.SecretKeySelector, 0) + for _, api := range restactionObj.Spec.API { + if api.EndpointRef != nil { + secretSelector := &core.SecretKeySelector{Name: api.EndpointRef.Name, Namespace: api.EndpointRef.Namespace} + if hasSecret(deleted, secretSelector) { // The endpoint copy has already been deleted + continue + } + err = secrets.Delete(ctx, rc, secretSelector) + if err != nil { + return fmt.Errorf("could not delete copy of endpoint secret object for restaction %s %s, endpoint %s %s: %w", restaction.Name, restaction.Namespace, secretSelector.Name, secretSelector.Namespace, err) + } + deleted = append(deleted, secretSelector) + } + } + + // Delete restaction copy + err = Delete(ctx, rc, restaction) + if err != nil { + return fmt.Errorf("could not delete copy of restaction object for %s %s: %w", restaction.Name, restaction.Namespace, err) + } + + return nil +} + +func hasSecret(list []*core.SecretKeySelector, obj *core.SecretKeySelector) bool { + for _, toCheck := range list { + if toCheck.Name == obj.Name && toCheck.Namespace == obj.Namespace { + return true + } + } + return false +} diff --git a/internal/helpers/restaction/restactions.go b/internal/helpers/restaction/restactions.go new file mode 100644 index 0000000..2bfe7c9 --- /dev/null +++ b/internal/helpers/restaction/restactions.go @@ -0,0 +1,113 @@ +package restaction + +import ( + "context" + + "github.com/krateoplatformops/authn/apis/core" + "github.com/krateoplatformops/authn/internal/helpers/kube/client" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" + + snowplowapis "github.com/krateoplatformops/snowplow/apis" + snowplow "github.com/krateoplatformops/snowplow/apis/templates/v1" +) + +// Add this to your client package +func newWithScheme(config *rest.Config, gv schema.GroupVersion, scheme *runtime.Scheme) (*rest.RESTClient, error) { + // Clone your existing New function but add scheme support + config = rest.CopyConfig(config) + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = serializer.NewCodecFactory(scheme) + config.UserAgent = rest.DefaultKubernetesUserAgent() + + return rest.RESTClientFor(config) +} + +func Get(ctx context.Context, rc *rest.Config, ref *core.ObjectRef) (*snowplow.RESTAction, error) { + cli, err := client.New(rc, schema.GroupVersion{Group: "templates.krateo.io", Version: "v1"}) + if err != nil { + return nil, err + } + + res := &snowplow.RESTAction{} + err = cli.Get(). + Resource("restactions"). + Namespace(ref.Namespace).Name(ref.Name). + Do(ctx). + Into(res) + + return res, err +} + +func CreateOrUpdate(ctx context.Context, rc *rest.Config, restaction *snowplow.RESTAction) error { + scheme := runtime.NewScheme() + snowplowapis.AddToScheme(scheme) + cli, err := newWithScheme(rc, schema.GroupVersion{Group: "templates.krateo.io", Version: "v1"}, scheme) + if err != nil { + return err + } + + // First try to get the resource + existingObj := &snowplow.RESTAction{} + err = cli.Get(). + Namespace(restaction.GetNamespace()). + Resource("restactions"). + Name(restaction.GetName()). + Do(ctx). + Into(existingObj) + + if err != nil { + if apierrors.IsNotFound(err) { + // Resource doesn't exist, create it + return cli.Post(). + Namespace(restaction.GetNamespace()). + Resource("restactions"). + Body(restaction). + Do(ctx). + Error() + } + return err // Return any other error + } + + restaction.ResourceVersion = existingObj.ResourceVersion + // Resource exists, update it + return cli.Put(). + Namespace(restaction.GetNamespace()). + Resource("restactions"). + Name(restaction.GetName()). + Body(restaction). + Do(ctx). + Error() +} + +func Update(ctx context.Context, rc *rest.Config, restaction *snowplow.RESTAction) error { + cli, err := client.New(rc, schema.GroupVersion{Group: "templates.krateo.io", Version: "v1"}) + if err != nil { + return err + } + return cli.Put(). + Namespace(restaction.GetNamespace()). + Resource("restactions"). + Name(restaction.Name). + Body(restaction). + Do(ctx). + Error() +} + +func Delete(ctx context.Context, rc *rest.Config, ref *core.ObjectRef) error { + cli, err := client.New(rc, schema.GroupVersion{Group: "templates.krateo.io", Version: "v1"}) + if err != nil { + return err + } + + return cli.Delete(). + Namespace(ref.Namespace). + Resource("restactions"). + Name(ref.Name). + Do(ctx). + Error() +} diff --git a/internal/routes/auth/oauth/login.go b/internal/routes/auth/oauth/login.go index 9e76a71..6866683 100644 --- a/internal/routes/auth/oauth/login.go +++ b/internal/routes/auth/oauth/login.go @@ -110,9 +110,13 @@ func (r *loginRoute) Handler() http.HandlerFunc { } additionalFieldstoReplace, err := restaction.Resolve(r.ctx, r.rc, restactionRef, uuid.New().String(), tok.AccessToken) if err != nil { - log.Err(err).Str("name", name).Msg("unable to resolve restaction") - encode.InternalError(wri, err) - return + log.Err(err).Str("name", name).Msg("unable to resolve restaction, retrying with legacy resolve (copy)") + additionalFieldstoReplace, err = restaction.LegacyResolve(r.ctx, r.rc, restactionRef, uuid.New().String(), tok.AccessToken) + if err != nil { + log.Err(err).Str("name", name).Msg("unable to resolve restaction, stopping") + encode.InternalError(wri, err) + return + } } log.Debug().Str("name", name).Msgf("values to replace: %s", additionalFieldstoReplace) userinfo, err = updateConfig(userinfo, additionalFieldstoReplace) diff --git a/internal/routes/auth/oidc/login.go b/internal/routes/auth/oidc/login.go index 6bb1f67..e0a543b 100644 --- a/internal/routes/auth/oidc/login.go +++ b/internal/routes/auth/oidc/login.go @@ -95,9 +95,13 @@ func (r *loginRoute) Handler() http.HandlerFunc { if cfg.RESTActionRef != nil { additionalFieldstoReplace, err := restaction.Resolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken) if err != nil { - log.Err(err).Str("name", name).Msg("unable to resolve restaction") - encode.InternalError(wri, err) - return + additionalFieldstoReplace, err = restaction.Resolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken) + log.Err(err).Str("name", name).Msg("unable to resolve restaction, retrying with legacy resolve (copy)") + if err != nil { + log.Err(err).Str("name", name).Msg("unable to resolve restaction, stopping") + encode.InternalError(wri, err) + return + } } log.Debug().Str("name", name).Msg("updating oidc idtoken") log.Debug().Str("name", name).Msgf("old idToken - name: %s - preferredUsername: %s - email: %s - groups: %s - avatarURL: %s", idToken.name, idToken.preferredUsername, idToken.email, idToken.groups, idToken.avatarURL) From dcee29facadbae9335726aa67fb4cd148953e33f Mon Sep 17 00:00:00 2001 From: FrancescoL96 Date: Tue, 7 Oct 2025 10:33:46 +0200 Subject: [PATCH 4/6] fix: improved fallback detection --- internal/routes/auth/oauth/login.go | 4 +++- internal/routes/auth/oidc/login.go | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/routes/auth/oauth/login.go b/internal/routes/auth/oauth/login.go index 6866683..642928c 100644 --- a/internal/routes/auth/oauth/login.go +++ b/internal/routes/auth/oauth/login.go @@ -109,7 +109,9 @@ func (r *loginRoute) Handler() http.HandlerFunc { return } additionalFieldstoReplace, err := restaction.Resolve(r.ctx, r.rc, restactionRef, uuid.New().String(), tok.AccessToken) - if err != nil { + value, ok := additionalFieldstoReplace["name"] + _, okk := additionalFieldstoReplace["name"].(string) + if err != nil || !ok || !okk || value == nil { log.Err(err).Str("name", name).Msg("unable to resolve restaction, retrying with legacy resolve (copy)") additionalFieldstoReplace, err = restaction.LegacyResolve(r.ctx, r.rc, restactionRef, uuid.New().String(), tok.AccessToken) if err != nil { diff --git a/internal/routes/auth/oidc/login.go b/internal/routes/auth/oidc/login.go index e0a543b..b3a67b1 100644 --- a/internal/routes/auth/oidc/login.go +++ b/internal/routes/auth/oidc/login.go @@ -94,9 +94,11 @@ func (r *loginRoute) Handler() http.HandlerFunc { log.Debug().Str("name", name).Msg("resolving restaction") if cfg.RESTActionRef != nil { additionalFieldstoReplace, err := restaction.Resolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken) - if err != nil { - additionalFieldstoReplace, err = restaction.Resolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken) + value, ok := additionalFieldstoReplace["name"] + _, okk := additionalFieldstoReplace["name"].(string) + if err != nil || !ok || !okk || value == nil { log.Err(err).Str("name", name).Msg("unable to resolve restaction, retrying with legacy resolve (copy)") + additionalFieldstoReplace, err = restaction.Resolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken) if err != nil { log.Err(err).Str("name", name).Msg("unable to resolve restaction, stopping") encode.InternalError(wri, err) From 2d671ab624987f576eb3eeffbc7f4eba6c136b08 Mon Sep 17 00:00:00 2001 From: FrancescoL96 Date: Mon, 27 Oct 2025 16:37:41 +0100 Subject: [PATCH 5/6] fix: oidc legacy resolve call --- internal/routes/auth/oidc/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/routes/auth/oidc/login.go b/internal/routes/auth/oidc/login.go index b3a67b1..495778b 100644 --- a/internal/routes/auth/oidc/login.go +++ b/internal/routes/auth/oidc/login.go @@ -98,7 +98,7 @@ func (r *loginRoute) Handler() http.HandlerFunc { _, okk := additionalFieldstoReplace["name"].(string) if err != nil || !ok || !okk || value == nil { log.Err(err).Str("name", name).Msg("unable to resolve restaction, retrying with legacy resolve (copy)") - additionalFieldstoReplace, err = restaction.Resolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken) + additionalFieldstoReplace, err = restaction.LegacyResolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken) if err != nil { log.Err(err).Str("name", name).Msg("unable to resolve restaction, stopping") encode.InternalError(wri, err) From d022d3ac2c36213b47cd1e81e809090d3a5cf251 Mon Sep 17 00:00:00 2001 From: FrancescoL96 Date: Mon, 27 Oct 2025 16:38:41 +0100 Subject: [PATCH 6/6] docs: added new oidc example with pagination --- README.md | 2 +- testdata/oidc-azure-pagination.yaml | 98 +++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 testdata/oidc-azure-pagination.yaml diff --git a/README.md b/README.md index d7fbe72..2a343a9 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ spec: namespace: krateo-system filter: .groups.value | map(.displayName) ``` -To obtain groups through the rest action, add `Directory.Read.All` in the `additionalScopes`. +To obtain groups through the rest action, add `Directory.Read.All` in the `additionalScopes`. There is a more complex example for the Microsoft Entra API that also uses pagination in the testdata folder (requires Authn >= 0.22.0). On AuthN: - To obtain the user avatar/profile image include `User.Read` in the `additionalScopes` field of the OIDCConfiguration custom resource; diff --git a/testdata/oidc-azure-pagination.yaml b/testdata/oidc-azure-pagination.yaml new file mode 100644 index 0000000..bf617bb --- /dev/null +++ b/testdata/oidc-azure-pagination.yaml @@ -0,0 +1,98 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: oidc-example-secret + namespace: demo-system +stringData: + clientSecret: +--- +apiVersion: "v1" +kind: Secret +metadata: + name: azure-entra + namespace: krateo-system +stringData: + server-url: https://graph.microsoft.com +--- +apiVersion: oidc.authn.krateo.io/v1alpha1 +kind: OIDCConfig +metadata: + name: oidc-example + namespace: demo-system +spec: + discoveryURL: https:///.well-known/openid-configuration + # Use these three fields if you do not have a discovery endpoint + # authorizationURL: authorization endpoint + # tokenURL: token endpoint + # userInfoURL: userinfo endpoint + redirectURI: http://localhost:8080/auth?kind=oidc # While any redirect URI can be used, the Krateo frontend requires /auth?kind=oidc + clientID: + clientSecret: + name: oidc-example-secret + namespace: demo-system + key: clientSecret + additionalScopes: # e.g., "User.Read Directory.Read.All" for Azure + restActionRef: # optional + name: test-rest-action + namespace: krateo-system + graphics: + icon: "fa-brands fa-windows" + displayName: "Login with Azure" + backgroundColor: "#4444ff" + textColor: "#ffffff" +--- +apiVersion: templates.krateo.io/v1 +kind: RESTAction +metadata: + name: test-rest-action + namespace: krateo-system +spec: + api: + - name: groups + verb: GET + continueOnError: false + errorKey: groupsError + headers: + - 'Accept: application/json' + - "${ \"Authorization: Bearer \" + .token }" + path: ${ .next // "v1.0/me/memberOf?$select=displayName&" } + endpointRef: + name: azure-entra + namespace: krateo-system + filter: | + .groups | if has("@odata.nextLink") then + { + res: (.value | map(.displayName)), + next: (."@odata.nextLink" | sub("https://graph.microsoft.com"; "")) + } + else + { + res: (.value | map(.displayName)) + } + end + - name: recursive + verb: GET + exportJwt: true + continueOnError: false + errorKey: recursiveError + dependsOn: + name: groups + iterator: | + .token as $token | .groups | if has("next") then [{next: .next, token: $token}] else [] end + headers: + - 'Accept: application/json' + path: | + ${ "/call?apiVersion=templates.krateo.io/v1&resource=restactions&name=test-rest-action&namespace=krateo-system&extras=" + ({token: .token, next: .next } | tostring | @uri ) } + endpointRef: + name: snowplow-endpoint + namespace: krateo-system + filter: .recursive.status + filter: | + .groups.res as $groups | + .recursive | + if type == "object" then + .groups += $groups + elif type == "null" then + {"groups": $groups} + end