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

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

commit 5366e7ecc16ba780c2b3faf3ef56ecb6e28e98fc
Author: lahiruj <[email protected]>
AuthorDate: Tue Jun 2 18:23:48 2026 -0400

    POSIX username generation and updated AMIE account provisioning to use 
POSIX allocator
---
 .../ACCESS/AMIE-Processor/handler/handler.go       |  12 ---
 .../ACCESS/AMIE-Processor/handler/posix_alloc.go   |  79 ++++++++++++++++
 .../AMIE-Processor/handler/posix_alloc_test.go     | 100 +++++++++++++++++++++
 .../handler/request_account_create.go              |  10 +--
 .../handler/request_project_create.go              |  23 ++++-
 .../request_project_create_integration_test.go     |  16 ++++
 6 files changed, 220 insertions(+), 20 deletions(-)

diff --git a/connectors/ACCESS/AMIE-Processor/handler/handler.go 
b/connectors/ACCESS/AMIE-Processor/handler/handler.go
index b4eca6db2..5153a4ac6 100644
--- a/connectors/ACCESS/AMIE-Processor/handler/handler.go
+++ b/connectors/ACCESS/AMIE-Processor/handler/handler.go
@@ -19,9 +19,7 @@ package handler
 
 import (
        "context"
-       "crypto/rand"
        "database/sql"
-       "encoding/hex"
        "errors"
        "fmt"
        "strconv"
@@ -193,16 +191,6 @@ func ensureOrganization(ctx context.Context, svc 
*service.Service, code, name st
        })
 }
 
-// generateTempPosixUsername returns a placeholder posix username for a
-// freshly provisioned ComputeClusterUser.
-//
-// TODO: replace with a real policy
-func generateTempPosixUsername() string {
-       var b [4]byte
-       _, _ = rand.Read(b[:])
-       return "amie-" + hex.EncodeToString(b[:])
-}
-
 // getInt64 reads a string-encoded integer from a packet body field. AMIE
 // transmits numeric fields like ServiceUnitsAllocated as JSON strings.
 func getInt64(body map[string]any, key string) (int64, error) {
diff --git a/connectors/ACCESS/AMIE-Processor/handler/posix_alloc.go 
b/connectors/ACCESS/AMIE-Processor/handler/posix_alloc.go
new file mode 100644
index 000000000..b1fb3ef6b
--- /dev/null
+++ b/connectors/ACCESS/AMIE-Processor/handler/posix_alloc.go
@@ -0,0 +1,79 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package handler
+
+import (
+       "context"
+       "errors"
+       "fmt"
+       "strconv"
+
+       "github.com/apache/airavata-custos/pkg/models"
+       "github.com/apache/airavata-custos/pkg/posix"
+       "github.com/apache/airavata-custos/pkg/service"
+)
+
+func allocateAndCreateClusterUser(ctx context.Context, svc *service.Service, 
clusterID, userID string) (*models.ComputeClusterUser, error) {
+       user, err := svc.GetUser(ctx, userID)
+       if err != nil {
+               return nil, fmt.Errorf("lookup user %q: %w", userID, err)
+       }
+
+       base, truncated, err := posix.BuildBase(user, posix.Prefix())
+       if err != nil {
+               _, _ = svc.CreateAuditEvent(ctx, &models.AuditEvent{
+                       EventType: "PosixUsernameUnbuildable",
+                       EntityID:  userID,
+                       Details:   err.Error(),
+               })
+               return nil, err
+       }
+       if truncated {
+               _, _ = svc.CreateAuditEvent(ctx, &models.AuditEvent{
+                       EventType: "PosixUsernameTruncated",
+                       EntityID:  userID,
+                       Details:   base,
+               })
+       }
+
+       for n := 0; n < posix.MaxCollisionSuffix; n++ {
+               candidate := base
+               if n > 0 {
+                       candidate = base + strconv.Itoa(n+1)
+               }
+               ccu, err := svc.CreateComputeClusterUser(ctx, 
&models.ComputeClusterUser{
+                       ComputeClusterID: clusterID,
+                       UserID:           userID,
+                       LocalUsername:    candidate,
+               })
+               if err == nil {
+                       return ccu, nil
+               }
+               if errors.Is(err, service.ErrAlreadyExists) {
+                       continue
+               }
+               return nil, err
+       }
+
+       _, _ = svc.CreateAuditEvent(ctx, &models.AuditEvent{
+               EventType: "PosixUsernameAllocatorExhausted",
+               EntityID:  userID,
+               Details:   base,
+       })
+       return nil, fmt.Errorf("posix username allocator exhausted for base 
%q", base)
+}
diff --git a/connectors/ACCESS/AMIE-Processor/handler/posix_alloc_test.go 
b/connectors/ACCESS/AMIE-Processor/handler/posix_alloc_test.go
new file mode 100644
index 000000000..b171c7cb6
--- /dev/null
+++ b/connectors/ACCESS/AMIE-Processor/handler/posix_alloc_test.go
@@ -0,0 +1,100 @@
+//go:build integration
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package handler
+
+import (
+       "context"
+       "fmt"
+       "strings"
+       "sync"
+       "testing"
+
+       "github.com/google/uuid"
+
+       "github.com/apache/airavata-custos/pkg/models"
+       "github.com/apache/airavata-custos/pkg/posix"
+)
+
+func TestAllocateAndCreateClusterUser_CollisionRetry(t *testing.T) {
+       database := setupTestDB(t)
+       svc := newTestCoreService(database)
+       ctx := context.Background()
+
+       org, err := svc.CreateOrganization(ctx, &models.Organization{
+               OriginatedID: uuid.NewString(),
+               Name:         "posix-alloc-test-org",
+       })
+       if err != nil {
+               t.Fatalf("create org: %v", err)
+       }
+
+       const n = 10
+       userIDs := make([]string, n)
+       for i := range userIDs {
+               u, err := svc.CreateUser(ctx, &models.User{
+                       OrganizationID: org.ID,
+                       FirstName:      "Collision",
+                       LastName:       "Target",
+                       Email:          fmt.Sprintf("col-%[email protected]", 
uuid.NewString()),
+               })
+               if err != nil {
+                       t.Fatalf("create user %d: %v", i, err)
+               }
+               userIDs[i] = u.ID
+       }
+
+       type result struct {
+               username string
+               err      error
+       }
+       results := make([]result, n)
+       var wg sync.WaitGroup
+       for i, uid := range userIDs {
+               wg.Add(1)
+               go func(idx int, userID string) {
+                       defer wg.Done()
+                       ccu, err := allocateAndCreateClusterUser(ctx, svc, 
testClusterID, userID)
+                       if err != nil {
+                               results[idx] = result{err: err}
+                               return
+                       }
+                       results[idx] = result{username: ccu.LocalUsername}
+               }(i, uid)
+       }
+       wg.Wait()
+
+       seen := make(map[string]bool)
+       for i, r := range results {
+               if r.err != nil {
+                       t.Errorf("goroutine %d: %v", i, r.err)
+                       continue
+               }
+               if seen[r.username] {
+                       t.Errorf("duplicate username %q across goroutines", 
r.username)
+               }
+               seen[r.username] = true
+               if !strings.HasPrefix(r.username, posix.Prefix()+"-") {
+                       t.Errorf("username %q does not start with %q", 
r.username, posix.Prefix()+"-")
+               }
+       }
+       if len(seen) != n {
+               t.Errorf("distinct usernames: got %d, want %d", len(seen), n)
+       }
+}
diff --git a/connectors/ACCESS/AMIE-Processor/handler/request_account_create.go 
b/connectors/ACCESS/AMIE-Processor/handler/request_account_create.go
index a42a3ac02..9896916f5 100644
--- a/connectors/ACCESS/AMIE-Processor/handler/request_account_create.go
+++ b/connectors/ACCESS/AMIE-Processor/handler/request_account_create.go
@@ -160,8 +160,8 @@ func (h *RequestAccountCreateHandler) ensureUser(ctx 
context.Context, body map[s
        return user, nil
 }
 
-// ensureComputeClusterUser returns the user's existing cluster mapping on the
-// configured cluster, or provisions a fresh one with a temp posix username.
+// ensureComputeClusterUser returns the user's existing cluster mapping or
+// provisions a new one via the POSIX allocator.
 func (h *RequestAccountCreateHandler) ensureComputeClusterUser(ctx 
context.Context, userID string) (*models.ComputeClusterUser, error) {
        existing, err := h.svc.ListComputeClusterUsersByUser(ctx, userID)
        if err != nil {
@@ -172,11 +172,7 @@ func (h *RequestAccountCreateHandler) 
ensureComputeClusterUser(ctx context.Conte
                        return &a, nil
                }
        }
-       return h.svc.CreateComputeClusterUser(ctx, &models.ComputeClusterUser{
-               UserID:           userID,
-               ComputeClusterID: h.clusterID,
-               LocalUsername:    generateTempPosixUsername(),
-       })
+       return allocateAndCreateClusterUser(ctx, h.svc, h.clusterID, userID)
 }
 
 // ensureMembership returns the existing (allocation, user) membership or
diff --git a/connectors/ACCESS/AMIE-Processor/handler/request_project_create.go 
b/connectors/ACCESS/AMIE-Processor/handler/request_project_create.go
index 3d7dff92e..178f2be4e 100644
--- a/connectors/ACCESS/AMIE-Processor/handler/request_project_create.go
+++ b/connectors/ACCESS/AMIE-Processor/handler/request_project_create.go
@@ -78,6 +78,14 @@ func (h *RequestProjectCreateHandler) Handle(ctx 
context.Context, tx *sql.Tx, pa
                return fmt.Errorf("request_project_create: audit CREATE_PERSON: 
%w", err)
        }
 
+       piClusterUser, err := h.ensurePIClusterUser(ctx, pi.ID)
+       if err != nil {
+               return fmt.Errorf("request_project_create: ensure PI cluster 
user: %w", err)
+       }
+       if err := h.auditSvc.Log(ctx, tx, packet.ID, eventID, 
model.AuditCreateAccount, "compute_cluster_user", piClusterUser.ID, 
piClusterUser.LocalUsername); err != nil {
+               return fmt.Errorf("request_project_create: audit CREATE_ACCOUNT 
(PI): %w", err)
+       }
+
        project, err := h.ensureProject(ctx, projectOriginatedID, grantNumber, 
pi.ID)
        if err != nil {
                return fmt.Errorf("request_project_create: ensure project: %w", 
err)
@@ -98,7 +106,7 @@ func (h *RequestProjectCreateHandler) Handle(ctx 
context.Context, tx *sql.Tx, pa
                "ProjectID":         project.ID,
                "GrantNumber":       grantNumber,
                "PiPersonID":        pi.ID,
-               "PiRemoteSiteLogin": piGlobalID,
+               "PiRemoteSiteLogin": piClusterUser.LocalUsername,
                "ResourceList":      getResourceList(body),
        }
        reply := map[string]any{"type": "notify_project_create", "body": 
replyBody}
@@ -142,6 +150,19 @@ func (h *RequestProjectCreateHandler) ensurePIUser(ctx 
context.Context, body map
        return user, nil
 }
 
+func (h *RequestProjectCreateHandler) ensurePIClusterUser(ctx context.Context, 
userID string) (*models.ComputeClusterUser, error) {
+       existing, err := h.svc.ListComputeClusterUsersByUser(ctx, userID)
+       if err != nil {
+               return nil, fmt.Errorf("list compute cluster users: %w", err)
+       }
+       for _, a := range existing {
+               if a.ComputeClusterID == h.clusterID {
+                       return &a, nil
+               }
+       }
+       return allocateAndCreateClusterUser(ctx, h.svc, h.clusterID, userID)
+}
+
 func (h *RequestProjectCreateHandler) ensureProject(ctx context.Context, 
originatedID, grantNumber, piID string) (*models.Project, error) {
        if p, err := h.svc.GetProjectByOriginatedID(ctx, originatedID); err == 
nil {
                return p, nil
diff --git 
a/connectors/ACCESS/AMIE-Processor/handler/request_project_create_integration_test.go
 
b/connectors/ACCESS/AMIE-Processor/handler/request_project_create_integration_test.go
index 92b5e345c..08110d500 100644
--- 
a/connectors/ACCESS/AMIE-Processor/handler/request_project_create_integration_test.go
+++ 
b/connectors/ACCESS/AMIE-Processor/handler/request_project_create_integration_test.go
@@ -187,6 +187,7 @@ func TestRequestProjectCreate_HappyPath(t *testing.T) {
 
        for _, action := range []model.AuditAction{
                model.AuditCreatePerson,
+               model.AuditCreateAccount,
                model.AuditCreateProject,
                model.AuditCreateAllocation,
                model.AuditReplySent,
@@ -196,6 +197,19 @@ func TestRequestProjectCreate_HappyPath(t *testing.T) {
                }
        }
 
+       var piCU struct {
+               LocalUsername string `db:"local_username"`
+       }
+       if err := database.Get(&piCU,
+               "SELECT local_username FROM compute_cluster_users WHERE user_id 
= ? AND compute_cluster_id = ?",
+               user.ID, testClusterID,
+       ); err != nil {
+               t.Fatalf("read PI compute_cluster_user: %v", err)
+       }
+       if piCU.LocalUsername == "" {
+               t.Errorf("PI compute_cluster_user.local_username: empty")
+       }
+
        if got, want := amie.lastReplyType(), "notify_project_create"; got != 
want {
                t.Fatalf("reply type: got %q, want %q", got, want)
        }
@@ -211,6 +225,8 @@ func TestRequestProjectCreate_HappyPath(t *testing.T) {
        }
        if got, _ := reply["PiRemoteSiteLogin"].(string); got == "" {
                t.Errorf("reply.PiRemoteSiteLogin: empty; required value")
+       } else if got != piCU.LocalUsername {
+               t.Errorf("reply.PiRemoteSiteLogin: got %q, want %q", got, 
piCU.LocalUsername)
        }
 }
 

Reply via email to