From f491659577ebe13b56725ca455b9b9aaa9fe46f9 Mon Sep 17 00:00:00 2001 From: Giuseppe Lo Presti Date: Fri, 28 Aug 2026 11:00:53 +0200 Subject: [PATCH 1/6] Config for hook --- user/rest/rest.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/user/rest/rest.go b/user/rest/rest.go index 524b45e..12657fb 100644 --- a/user/rest/rest.go +++ b/user/rest/rest.go @@ -89,6 +89,11 @@ type config struct { TargetAPI string `mapstructure:"target_api" docs:"authorization-service-api"` // The time in seconds between bulk fetch of user accounts UserFetchInterval int `mapstructure:"user_fetch_interval" docs:"3600"` + + // Endpoint of the lifecycle daemon + LifecycleEndpoint string `mapstructure:"lifecycle_endpoint" docs:"https://cbox-lifecycle.cern.ch"` + // Shared secret to be passed as bearer token to the lifecycle daemon + LifecycleSecret string `mapstructure:"lifecycle_secret"` } func (c *config) ApplyDefaults() { @@ -311,7 +316,6 @@ func (m *manager) parseAndCacheUser(ctx context.Context, i *Identity) (*userpb.U log.Error().Err(err).Str("user", u.Username).Msg("rest: error fetching cached user details to check if the user has left CERN") } else { if cachedUser.Status != userpb.UserStatus_USER_STATUS_EXPIRING { - log.Info().Str("user", u.Username).Msg("rest: user has left CERN, notifying lifecycle manager") if err := m.notifyLifecycleManager(ctx, u); err != nil { log.Error().Err(err).Str("user", u.Username).Msg("rest: error notifying lifecycle manager about user leaving CERN") } @@ -350,7 +354,14 @@ func (m *manager) fetchExternalIdentities(ctx context.Context, email string) ([] } func (m *manager) notifyLifecycleManager(ctx context.Context, user *userpb.User) error { - // TODO(lopresti) notify our lifecycle daemon that the user has left CERN + log := appctx.GetLogger(ctx) + // call the lifecycle daemon if configured + if m.conf.LifecycleEndpoint != "" && m.conf.LifecycleSecret != "" { + + } + else { + log.Warning().Str("user", u.Username).Msg("rest: user has left CERN, no lifecycle endpoint configured to notify") + } return nil } From 7c76a8f8bf43ec599055612b293d7f88a5ef2fb8 Mon Sep 17 00:00:00 2001 From: Pablo Medina Ramos Date: Fri, 28 Aug 2026 11:55:08 +0200 Subject: [PATCH 2/6] Implement lifecycle daemon notification when a user leaves CERN PATCH {lifecycle_endpoint}/cbox/account/{user} with a GracePeriod subscription status, using lifecycle_secret as bearer token. If the endpoint or the secret is not configured, only log a warning. --- user/rest/rest.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/user/rest/rest.go b/user/rest/rest.go index 12657fb..3264c6e 100644 --- a/user/rest/rest.go +++ b/user/rest/rest.go @@ -22,6 +22,7 @@ import ( "context" "errors" "fmt" + "net/http" neturl "net/url" "os" "os/signal" @@ -357,10 +358,25 @@ func (m *manager) notifyLifecycleManager(ctx context.Context, user *userpb.User) log := appctx.GetLogger(ctx) // call the lifecycle daemon if configured if m.conf.LifecycleEndpoint != "" && m.conf.LifecycleSecret != "" { + url := fmt.Sprintf("%s/cbox/account/%s", m.conf.LifecycleEndpoint, neturl.PathEscape(user.Username)) + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, strings.NewReader(`{"subscriptionStatus": "GracePeriod"}`)) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+m.conf.LifecycleSecret) + req.Header.Set("Content-Type", "application/json") - } - else { - log.Warning().Str("user", u.Username).Msg("rest: user has left CERN, no lifecycle endpoint configured to notify") + res, err := (&http.Client{Timeout: 10 * time.Second}).Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode > 299 { + return fmt.Errorf("rest: lifecycle daemon returned %s for user %s", res.Status, user.Username) + } + } else { + log.Warn().Str("user", user.Username).Msg("rest: user has left CERN, no lifecycle endpoint configured to notify") } return nil } From 5ccd7bec48cd4aad83c0775f3a1a4ddd0b6d8a9b Mon Sep 17 00:00:00 2001 From: Pablo Medina Ramos Date: Fri, 28 Aug 2026 16:32:19 +0200 Subject: [PATCH 3/6] Log the lifecycle daemon response The daemon replies with a JSON body describing the resulting account state; log it regardless of the HTTP status code. --- user/rest/rest.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user/rest/rest.go b/user/rest/rest.go index 3264c6e..eb4ac4e 100644 --- a/user/rest/rest.go +++ b/user/rest/rest.go @@ -22,6 +22,7 @@ import ( "context" "errors" "fmt" + "io" "net/http" neturl "net/url" "os" @@ -372,6 +373,9 @@ func (m *manager) notifyLifecycleManager(ctx context.Context, user *userpb.User) } defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + log.Info().Str("user", user.Username).Int("status", res.StatusCode).Str("response", string(body)).Msg("rest: lifecycle daemon response") + if res.StatusCode < 200 || res.StatusCode > 299 { return fmt.Errorf("rest: lifecycle daemon returned %s for user %s", res.Status, user.Username) } From 2475a2e6e1ca256d02ee2c438aa8da73f32ff5f4 Mon Sep 17 00:00:00 2001 From: Pablo Medina Ramos Date: Fri, 28 Aug 2026 11:55:19 +0200 Subject: [PATCH 4/6] Do not advertise a default lifecycle endpoint The endpoint differs between qa and prod, so it must be set explicitly per environment; drop the docs default to avoid pointing at prod by mistake. --- user/rest/rest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/rest/rest.go b/user/rest/rest.go index eb4ac4e..95e910c 100644 --- a/user/rest/rest.go +++ b/user/rest/rest.go @@ -92,8 +92,8 @@ type config struct { // The time in seconds between bulk fetch of user accounts UserFetchInterval int `mapstructure:"user_fetch_interval" docs:"3600"` - // Endpoint of the lifecycle daemon - LifecycleEndpoint string `mapstructure:"lifecycle_endpoint" docs:"https://cbox-lifecycle.cern.ch"` + // Endpoint of the lifecycle daemon, set per environment + LifecycleEndpoint string `mapstructure:"lifecycle_endpoint"` // Shared secret to be passed as bearer token to the lifecycle daemon LifecycleSecret string `mapstructure:"lifecycle_secret"` } From d47050dc9804ec635262b33a33f9a55c669a5386 Mon Sep 17 00:00:00 2001 From: Pablo Medina Ramos Date: Fri, 28 Aug 2026 16:33:27 +0200 Subject: [PATCH 5/6] Require an exact 200 from the lifecycle daemon The daemon returns 200 on success for this PATCH, so treat anything else as an error and include its response body in the error message. --- user/rest/rest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/rest/rest.go b/user/rest/rest.go index 95e910c..ce69392 100644 --- a/user/rest/rest.go +++ b/user/rest/rest.go @@ -376,8 +376,8 @@ func (m *manager) notifyLifecycleManager(ctx context.Context, user *userpb.User) body, _ := io.ReadAll(res.Body) log.Info().Str("user", user.Username).Int("status", res.StatusCode).Str("response", string(body)).Msg("rest: lifecycle daemon response") - if res.StatusCode < 200 || res.StatusCode > 299 { - return fmt.Errorf("rest: lifecycle daemon returned %s for user %s", res.Status, user.Username) + if res.StatusCode != http.StatusOK { + return fmt.Errorf("rest: lifecycle daemon returned %s for user %s: %s", res.Status, user.Username, body) } } else { log.Warn().Str("user", user.Username).Msg("rest: user has left CERN, no lifecycle endpoint configured to notify") From ae08115d0623dac4888da02a82e12b85e5088ba1 Mon Sep 17 00:00:00 2001 From: Giuseppe Lo Presti Date: Fri, 28 Aug 2026 17:01:42 +0200 Subject: [PATCH 6/6] Remove duplication --- user/rest/rest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/rest/rest.go b/user/rest/rest.go index ce69392..94a6ed7 100644 --- a/user/rest/rest.go +++ b/user/rest/rest.go @@ -377,7 +377,7 @@ func (m *manager) notifyLifecycleManager(ctx context.Context, user *userpb.User) log.Info().Str("user", user.Username).Int("status", res.StatusCode).Str("response", string(body)).Msg("rest: lifecycle daemon response") if res.StatusCode != http.StatusOK { - return fmt.Errorf("rest: lifecycle daemon returned %s for user %s: %s", res.Status, user.Username, body) + return fmt.Errorf("rest: lifecycle daemon failed with status %s", res.Status) } } else { log.Warn().Str("user", user.Username).Msg("rest: user has left CERN, no lifecycle endpoint configured to notify")