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) } }
