You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/07/24 05:07:52 UTC

(airavata-custos) branch pearc26 updated: Enable auto-approval for access requests based on allowlisted emails

This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch pearc26
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git


The following commit(s) were added to refs/heads/pearc26 by this push:
     new 7a417c12b Enable auto-approval for access requests based on allowlisted emails
7a417c12b is described below

commit 7a417c12b3ba81053e6a814af52d269007fdc35e
Author: lahiruj <la...@gmail.com>
AuthorDate: Fri Jul 24 01:07:45 2026 -0400

    Enable auto-approval for access requests based on allowlisted emails
---
 cmd/server/main.go                             |  5 +++++
 pkg/service/access_request.go                  | 28 ++++++++++++++++++++++++++
 pkg/service/access_request_integration_test.go | 28 ++++++++++++++++++++++++++
 pkg/service/service.go                         | 17 ++++++++++++++++
 4 files changed, 78 insertions(+)

diff --git a/cmd/server/main.go b/cmd/server/main.go
index f5c9e90a3..85e9661e0 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -163,6 +163,11 @@ func run() error {
 	}
 	svc.SetMailer(mailer)
 
+	if raw := strings.TrimSpace(os.Getenv("AUTO_APPROVE_EMAILS")); raw != "" {
+		svc.SetAutoApprove(strings.Split(raw, ","), os.Getenv("CUSTOS_BOOTSTRAP_ADMIN_EMAIL"))
+		slog.Info("access-request auto-approve enabled", "count", len(strings.Split(raw, ",")))
+	}
+
 	// identity.Middleware sits in front of the router-backed server, so every
 	// gated route sees a verified caller + privilege set on ctx.
 	authed := identity.Middleware(verifier, svc, router.PublicPaths(), router.TokenPathMatcher(), srv)
diff --git a/pkg/service/access_request.go b/pkg/service/access_request.go
index 768fdf8d7..fa9a0a7bd 100644
--- a/pkg/service/access_request.go
+++ b/pkg/service/access_request.go
@@ -113,9 +113,37 @@ func (s *Service) CreateAccessRequest(ctx context.Context, req *models.AccessReq
 	}); err != nil {
 		return nil, fmt.Errorf("create access request: %w", err)
 	}
+
+	if s.autoApproveEmails[strings.ToLower(strings.TrimSpace(req.Email))] {
+		if approved := s.autoApproveRequest(ctx, req); approved != nil {
+			return approved, nil
+		}
+	}
 	return req, nil
 }
 
+// autoApproveRequest approves a just-created request on behalf of the configured
+// approver with a far-future expiry, so known team members skip the manual queue.
+// Returns nil (leaving the request PENDING for a manual approve) if the approver
+// is unknown or provisioning fails.
+func (s *Service) autoApproveRequest(ctx context.Context, req *models.AccessRequest) *models.AccessRequest {
+	approver, err := s.users.FindByEmail(ctx, s.autoApproveApprover)
+	if err != nil || approver == nil {
+		slog.Warn("auto-approve skipped: approver not found", "approver", s.autoApproveApprover, "email", req.Email)
+		return nil
+	}
+	// Membership end_time is TIMESTAMP(6), capped at 2038-01-19, so this near-max
+	// is the practical "no expiry" for team members.
+	exp := time.Date(2038, 1, 1, 0, 0, 0, 0, time.UTC)
+	approved, err := s.ApproveAccessRequest(ctx, req.ID, approver.ID, &exp)
+	if err != nil {
+		slog.Warn("auto-approve failed, request left pending", "email", req.Email, "error", err)
+		return nil
+	}
+	slog.Info("auto-approved access request", "email", req.Email)
+	return approved
+}
+
 // GetLatestAccessRequestBySub returns the newest access request submitted by
 // the given token subject. Returns ErrNotFound when the subject has none.
 func (s *Service) GetLatestAccessRequestBySub(ctx context.Context, sub string) (*models.AccessRequest, error) {
diff --git a/pkg/service/access_request_integration_test.go b/pkg/service/access_request_integration_test.go
index 2bd522a3e..ab6abaeb5 100644
--- a/pkg/service/access_request_integration_test.go
+++ b/pkg/service/access_request_integration_test.go
@@ -253,6 +253,34 @@ func TestApproveAccessRequest_HappyPath(t *testing.T) {
 	}
 }
 
+// An allowlisted email is approved on creation, not left PENDING, and provisioning
+// runs (created_user_id is set). Uppercase entry proves the match is case-insensitive.
+func TestCreateAccessRequest_AutoApprovesAllowlistedEmail(t *testing.T) {
+	env := setupAccessEnv(t)
+	admin, err := env.svc.GetUser(ctx(), env.adminID)
+	if err != nil {
+		t.Fatalf("get admin: %v", err)
+	}
+	sub := "sub-" + uuid.NewString()[:8]
+	req := newAccessRequestFor(env, sub)
+	env.svc.SetAutoApprove([]string{strings.ToUpper(req.Email)}, admin.Email)
+	defer env.svc.SetAutoApprove(nil, "")
+
+	created, err := env.svc.CreateAccessRequest(ctx(), req)
+	if err != nil {
+		t.Fatalf("create: %v", err)
+	}
+	if created.Status != models.AccessRequestApproved {
+		t.Fatalf("status = %s, want APPROVED (auto-approved on create)", created.Status)
+	}
+	if created.ApproverID != env.adminID {
+		t.Errorf("approver = %q, want admin %q", created.ApproverID, env.adminID)
+	}
+	if created.CreatedUserID == "" {
+		t.Error("created_user_id empty; provisioning did not run on auto-approve")
+	}
+}
+
 func TestApproveAccessRequest_ReusesExistingUser(t *testing.T) {
 	env := setupAccessEnv(t)
 	sub := "sub-" + uuid.NewString()[:8]
diff --git a/pkg/service/service.go b/pkg/service/service.go
index dd2e71d37..f05838de6 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -20,6 +20,7 @@ package service
 import (
 	"context"
 	"database/sql"
+	"strings"
 	"time"
 
 	"github.com/google/uuid"
@@ -64,6 +65,8 @@ type Service struct {
 	auditTraces         store.AuditTraceStore
 	identityCache       *identityCache
 	mailer              email.Mailer
+	autoApproveEmails   map[string]bool
+	autoApproveApprover string
 }
 
 // SetMailer configures notification mail; unset means messages are dropped.
@@ -73,6 +76,20 @@ func (s *Service) SetMailer(m email.Mailer) {
 	}
 }
 
+// SetAutoApprove enables auto-approval of access requests whose email is in the
+// list, recorded as approved by approverEmail. An empty list disables it. Used
+// to fast-track known team members through the trial-account flow.
+func (s *Service) SetAutoApprove(emails []string, approverEmail string) {
+	m := make(map[string]bool, len(emails))
+	for _, e := range emails {
+		if e = strings.TrimSpace(strings.ToLower(e)); e != "" {
+			m[e] = true
+		}
+	}
+	s.autoApproveEmails = m
+	s.autoApproveApprover = strings.TrimSpace(strings.ToLower(approverEmail))
+}
+
 // New constructs a Service backed by the supplied database handle.
 // Stores are instantiated internally using the default MySQL implementations.
 func New(database *sqlx.DB, eventBus *events.Bus) *Service {