Skip to content
This repository was archived by the owner on Sep 16, 2026. It is now read-only.
Open
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
189 changes: 176 additions & 13 deletions pkg/agent/handler/accessrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import (
"time"

agentcache "github.com/Axway/agent-sdk/pkg/agent/cache"
"github.com/Axway/agent-sdk/pkg/agent/provisioningwebhook"
"github.com/Axway/agent-sdk/pkg/api"
apiv1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1"
management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1"
defs "github.com/Axway/agent-sdk/pkg/apic/definitions"
prov "github.com/Axway/agent-sdk/pkg/apic/provisioning"
"github.com/Axway/agent-sdk/pkg/config"
"github.com/Axway/agent-sdk/pkg/util"
"github.com/Axway/agent-sdk/pkg/util/log"
"github.com/Axway/agent-sdk/pkg/watchmanager/proto"
Expand All @@ -35,6 +38,8 @@ type accessRequestHandler struct {
encryptSchema encryptSchemaFunc
customUnitHandler customUnitHandler
retryCount int
webhookCfg config.ProvisioningWebhookEndpointConfig
webhookClient api.Client
}

func WithAccessRequestRetryCount(rc int) func(c *accessRequestHandler) {
Expand All @@ -43,6 +48,15 @@ func WithAccessRequestRetryCount(rc int) func(c *accessRequestHandler) {
}
}

// WithAccessRequestProvisioningWebhook configures the webhook the handler calls instead of its own
// registered Provisioning implementation, when cfg.IsConfigured()
func WithAccessRequestProvisioningWebhook(cfg config.ProvisioningWebhookEndpointConfig, client api.Client) func(c *accessRequestHandler) {
return func(c *accessRequestHandler) {
c.webhookCfg = cfg
c.webhookClient = client
Comment thread
jcollins-axway marked this conversation as resolved.
}
}

// NewAccessRequestHandler creates a Handler for Access Requests
func NewAccessRequestHandler(prov prov.AccessProvisioner, cache agentcache.Manager, client client, customUnitHandler customUnitHandler, opts ...func(c *accessRequestHandler)) Handler {
arh := &accessRequestHandler{
Expand All @@ -51,6 +65,7 @@ func NewAccessRequestHandler(prov prov.AccessProvisioner, cache agentcache.Manag
client: client,
encryptSchema: encryptSchema,
customUnitHandler: customUnitHandler,
webhookCfg: config.NewProvisioningWebhookEndpointConfig("provisioningWebhook.accessRequest"),
}
for _, o := range opts {
o(arh)
Expand All @@ -63,6 +78,9 @@ func (h *accessRequestHandler) ShouldHandle(ctx context.Context, event *proto.Ev
if action == proto.Event_SUBRESOURCEUPDATED && event.Metadata.GetSubresource() == defs.XAgentDetails {
return true
}
if action == proto.Event_SUBRESOURCEUPDATED && event.Metadata.GetSubresource() == defs.XWebhookDetails {
return h.webhookCfg.IsConfigured()
}
Comment thread
jcollins-axway marked this conversation as resolved.
if h.prov == nil || h.shouldIgnore(action, event.Metadata) {
return false
}
Expand Down Expand Up @@ -114,6 +132,16 @@ func (h *accessRequestHandler) Handle(ctx context.Context, meta *proto.EventMeta
return nil
}

if action == proto.Event_SUBRESOURCEUPDATED && meta.GetSubresource() == defs.XWebhookDetails {
if webhookDispatchedFor(ar, webhookOperationProvision) {
return h.postWebhookProvisionProcess(log, ar)
}
if webhookDispatchedFor(ar, webhookOperationDeprovision) {
h.postWebhookDeprovisionProcess(log, ar)
}
return nil
}

// add or update the cache with the access request
// migrated access request is not added to cache until processed for Pending
if (action == proto.Event_CREATED || action == proto.Event_UPDATED) && ar.Spec.AccessRequest == "" {
Expand All @@ -129,6 +157,13 @@ func (h *accessRequestHandler) Handle(ctx context.Context, meta *proto.EventMeta
return nil
}

if ok := h.shouldProvisionWebhook(ar.Status, ar.Metadata.State, h.webhookCfg); ok {
log.Trace("processing resource in pending status via provisioning webhook")
mar := h.getMigratingAccessRequest(ar)
h.onWebhookProvision(ctx, log, ar, mar)
return nil
}

if ok := h.shouldProcessPending(ar.Status, ar.Metadata.State); ok {
mar := h.getMigratingAccessRequest(ar)

Expand Down Expand Up @@ -161,6 +196,12 @@ func (h *accessRequestHandler) Handle(ctx context.Context, meta *proto.EventMeta
return err
}

if ok := h.shouldDeprovisionWebhook(ar.Status, ar.Metadata.State, ar.Finalizers, h.webhookCfg); ok {
log.Trace("processing resource in deleting state via provisioning webhook")
h.onWebhookDeprovision(ctx, log, ar)
return nil
}

if ok := h.shouldProcessDeleting(ar.Status, ar.Metadata.State, ar.Finalizers); ok {
log.Trace("processing resource in deleting state")
h.onDeleting(ctx, ar)
Expand All @@ -169,39 +210,50 @@ func (h *accessRequestHandler) Handle(ctx context.Context, meta *proto.EventMeta
return nil
}

func (h *accessRequestHandler) onPending(ctx context.Context, ar *management.AccessRequest, mar *apiv1.ResourceInstance) *management.AccessRequest {
// buildAccessRequest fetches the managed app and access request definition, then builds the full
// provisioning request - shared by the classic and webhook-dispatch paths, which only differ in how they
// report a failure here. app/ard are also returned since callers need them beyond just building req
// (quota enforcement, secret data encryption).
func (h *accessRequestHandler) buildAccessRequest(ctx context.Context, ar *management.AccessRequest, mar *apiv1.ResourceInstance) (*management.ManagedApplication, *management.AccessRequestDefinition, *provAccReq, error) {
log := getLoggerFromContext(ctx)

app, err := h.getManagedApp(ctx, ar)
if err != nil {
log.WithError(err).Error("error getting managed app")
h.onError(ctx, ar, err)
return ar
return nil, nil, nil, err
}

// check the application status
if app.Status.Level != prov.Success.String() {
err = fmt.Errorf("error can't handle access request when application is not yet successful")
log.WithError(err).Error("error processing access request")
h.onError(ctx, ar, err)
return ar
err := errors.New("error can't handle access request when application is not yet successful")
log.WithError(err).Error("error checking application status")
return nil, nil, nil, err
}

ard, err := h.getARD(ctx, ar)
if err != nil {
log.WithError(err).Errorf("error getting access request definition")
h.onError(ctx, ar, err)
return ar
log.WithError(err).Error("error getting access request definition")
return nil, nil, nil, err
}

req, err := h.newReq(ctx, ar, mar, util.GetAgentDetails(app))
if err != nil {
log.WithError(err).Error("error getting resource details")
h.onError(ctx, ar, err)
return ar
return nil, nil, nil, err
}

updateDataFromEnumMap(ar.Spec.Data, ard.Spec.Schema)

return app, ard, req, nil
}

func (h *accessRequestHandler) onPending(ctx context.Context, ar *management.AccessRequest, mar *apiv1.ResourceInstance) *management.AccessRequest {
app, ard, req, err := h.buildAccessRequest(ctx, ar, mar)
if err != nil {
h.onError(ctx, ar, err)
return ar
}

data := map[string]interface{}{}
status, accessData := h.provision(req)

Expand Down Expand Up @@ -258,6 +310,83 @@ func (h *accessRequestHandler) onPending(ctx context.Context, ar *management.Acc
return ar
}

// onWebhookProvision dispatches the provision request to the configured webhook instead of calling this
// agent's own registered Provisioning implementation. Self-contained (builds its own request, persists its
// own result) since it's called directly from Handle, before onPending's classic-only path.
func (h *accessRequestHandler) onWebhookProvision(ctx context.Context, log log.FieldLogger, ar *management.AccessRequest, mar *apiv1.ResourceInstance) {
if webhookDispatchedFor(ar, webhookOperationProvision) {
return
}

_, _, req, err := h.buildAccessRequest(ctx, ar, mar)
if err != nil {
h.onError(ctx, ar, err)
h.client.CreateSubResource(ar.ResourceMeta, ar.SubResources)
return
}

if err := provisioningwebhook.Dispatch(h.webhookClient, h.webhookCfg, newWebhookAccessRequest(webhookOperationProvision, *req)); err != nil {
log.WithError(err).Error("provisioning webhook dispatch failed")
h.onError(ctx, ar, err)
h.client.CreateSubResource(ar.ResourceMeta, ar.SubResources)
return
}
markWebhookDispatched(ar, webhookOperationProvision)
h.client.CreateSubResource(ar.ResourceMeta, ar.SubResources)
}

// postWebhookProvisionProcess mirrors the webhook's x-webhook-details into x-agent-details, and adds the
// finalizer once status reflects success - status is the webhook's own responsibility (see
// docs/discovery/provisioning-webhook.md), so ar.Status already reflects the outcome by the time this
// runs. On success, also completes any pending AccessRequest migration/transfer: getMigratingAccessRequest
// is a stateless lookup keyed off ar.Spec.AccessRequest, so it's safe to resolve fresh here rather than
// needing whatever mar onWebhookProvision saw at dispatch time. ar is dropped from cache after that (not
// re-added) since the x-agent-details write below lets the already-decoupled accessRequestCacheHandler
// pick it back up fresh. Only called when the dispatched operation was a provision - see Handle.
func (h *accessRequestHandler) postWebhookProvisionProcess(log log.FieldLogger, ar *management.AccessRequest) error {
if ar.Status.Level == prov.Success.String() {
if !hasFinalizer(ar.Finalizers, arFinalizer) {
// only add finalizer on success
ri, _ := ar.AsInstance()
h.client.UpdateResourceFinalizer(ri, arFinalizer, "", true)
}

if mar := h.getMigratingAccessRequest(ar); mar != nil {
h.client.UpdateResourceFinalizer(mar, arFinalizer, "", false)
if err := h.client.DeleteResourceInstance(mar); err != nil {
log.WithError(err).Error("failed to delete migrating access request")
}
h.cache.DeleteAccessRequest(ar.Metadata.ID)
}
}
if mirrorWebhookDetails(ar) {
return h.client.CreateSubResource(ar.ResourceMeta, ar.SubResources)
}
return nil
}

// postWebhookDeprovisionProcess removes the finalizer (if still present) and drops the access request
// from cache once the webhook reports it successfully completed a deprovision. ar.Status can't be used
// for this - it's whatever it already was when shouldDeprovisionWebhook let the event through, not a
// signal from the webhook - so success/failure is read from the reserved "status" (and, on failure,
// "message") keys the webhook writes inside x-webhook-details itself. Status itself is the webhook's own
// responsibility (see docs/discovery/provisioning-webhook.md), so on failure this only logs - it doesn't
// write anything. No mirroring here - deprovision typically has no data to report. Only called when the
// dispatched operation was a deprovision - see Handle.
func (h *accessRequestHandler) postWebhookDeprovisionProcess(log log.FieldLogger, ar *management.AccessRequest) {
if webhookDetailsValue(ar, webhookStatusKey) != webhookStatusSuccess {
message := webhookDetailsValue(ar, webhookMessageKey)
log.WithField("message", message).Error("provisioning webhook reported deprovision failure")
return
}

if hasFinalizer(ar.Finalizers, arFinalizer) {
ri, _ := ar.AsInstance()
h.client.UpdateResourceFinalizer(ri, arFinalizer, "", false)
}
h.cache.DeleteAccessRequest(ar.Metadata.ID)
}

func (h *accessRequestHandler) provision(par *provAccReq) (prov.RequestStatus, prov.AccessData) {
status, accessData := h.prov.AccessRequestProvision(par)
if status.GetStatus() == prov.Success {
Expand Down Expand Up @@ -311,7 +440,6 @@ func (h *accessRequestHandler) onDeleting(ctx context.Context, ar *management.Ac
}

status := h.prov.AccessRequestDeprovision(req)

if status.GetStatus() == prov.Success {
h.client.UpdateResourceFinalizer(ri, arFinalizer, "", false)
h.cache.DeleteAccessRequest(ri.Metadata.ID)
Expand All @@ -323,6 +451,41 @@ func (h *accessRequestHandler) onDeleting(ctx context.Context, ar *management.Ac
}
}

// onWebhookDeprovision dispatches the deprovision request to the configured webhook instead of calling
// this agent's own registered Provisioning implementation. Self-contained (builds its own request,
// persists its own result) since it's called directly from Handle, before onDeleting's classic-only path.
func (h *accessRequestHandler) onWebhookDeprovision(ctx context.Context, log log.FieldLogger, ar *management.AccessRequest) {
if webhookDispatchedFor(ar, webhookOperationDeprovision) {
return
}

app, err := h.getManagedApp(ctx, ar)
if err != nil {
log.WithError(err).Error("error getting managed app")
h.onError(ctx, ar, err)
h.client.CreateSubResource(ar.ResourceMeta, ar.SubResources)
return
}

req, err := h.newReq(ctx, ar, nil, util.GetAgentDetails(app))
if err != nil {
log.WithError(err).Debug("removing finalizers on the access request")
ri, _ := ar.AsInstance()
h.client.UpdateResourceFinalizer(ri, arFinalizer, "", false)
h.cache.DeleteAccessRequest(ri.Metadata.ID)
return
}

if err := provisioningwebhook.Dispatch(h.webhookClient, h.webhookCfg, newWebhookAccessRequest(webhookOperationDeprovision, *req)); err != nil {
log.WithError(err).Error("provisioning webhook dispatch failed")
h.onError(ctx, ar, err)
h.client.CreateSubResource(ar.ResourceMeta, ar.SubResources)
return
}
markWebhookDispatched(ar, webhookOperationDeprovision)
h.client.CreateSubResource(ar.ResourceMeta, ar.SubResources)
}

func (h *accessRequestHandler) getManagedApp(_ context.Context, ar *management.AccessRequest) (*management.ManagedApplication, error) {
app := management.NewManagedApplication(ar.Spec.ManagedApplication, ar.Metadata.Scope.Name)
ri, err := h.client.GetResource(app.GetSelfLink())
Expand Down
Loading
Loading