This is an automated email from the ASF dual-hosted git repository. DImuthuUpe pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit 4ea551e4a3be0c3f46ac4e4c4bc35ae2b04d8b33 Author: lahiruj <[email protected]> AuthorDate: Thu May 21 16:31:35 2026 -0400 Drop UserDN and UserMerge models and related services from core --- ...ns.down.sql => 000017_user_identities.down.sql} | 1 - ...er_dns.up.sql => 000017_user_identities.up.sql} | 14 --- internal/db/migrations/000018_user_merges.down.sql | 18 --- internal/db/migrations/000018_user_merges.up.sql | 30 ----- internal/server/server.go | 82 +------------ internal/store/store.go | 28 ----- internal/store/user_dn_store.go | 103 ----------------- internal/store/user_merge_store.go | 77 ------------- pkg/events/types.go | 7 -- pkg/events/user_dn_subscribe.go | 57 --------- pkg/models/identity.go | 10 -- pkg/models/project.go | 11 -- pkg/service/service.go | 8 -- pkg/service/user_dn.go | 127 --------------------- pkg/service/user_merge.go | 64 ++--------- 15 files changed, 10 insertions(+), 627 deletions(-) diff --git a/internal/db/migrations/000017_user_identities_and_user_dns.down.sql b/internal/db/migrations/000017_user_identities.down.sql similarity index 96% rename from internal/db/migrations/000017_user_identities_and_user_dns.down.sql rename to internal/db/migrations/000017_user_identities.down.sql index 72d75c517..08b342405 100644 --- a/internal/db/migrations/000017_user_identities_and_user_dns.down.sql +++ b/internal/db/migrations/000017_user_identities.down.sql @@ -15,5 +15,4 @@ -- specific language governing permissions and limitations -- under the License. -DROP TABLE IF EXISTS user_dns; DROP TABLE IF EXISTS user_identities; diff --git a/internal/db/migrations/000017_user_identities_and_user_dns.up.sql b/internal/db/migrations/000017_user_identities.up.sql similarity index 76% rename from internal/db/migrations/000017_user_identities_and_user_dns.up.sql rename to internal/db/migrations/000017_user_identities.up.sql index 313f1de71..249468c89 100644 --- a/internal/db/migrations/000017_user_identities_and_user_dns.up.sql +++ b/internal/db/migrations/000017_user_identities.up.sql @@ -36,17 +36,3 @@ CREATE TABLE IF NOT EXISTS user_identities KEY idx_user_identities_user (user_id), CONSTRAINT fk_user_identities_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; - --- A DN is a globally-unique credential. UNIQUE on dn alone subsumes --- (user_id, dn), so the composite index is omitted. -CREATE TABLE IF NOT EXISTS user_dns -( - id VARCHAR(255) NOT NULL, - user_id VARCHAR(255) NOT NULL, - dn VARCHAR(512) NOT NULL, - created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), - PRIMARY KEY (id), - UNIQUE KEY uq_user_dns_dn (dn), - KEY idx_user_dns_user (user_id), - CONSTRAINT fk_user_dns_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE -) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; diff --git a/internal/db/migrations/000018_user_merges.down.sql b/internal/db/migrations/000018_user_merges.down.sql deleted file mode 100644 index bc65c15a1..000000000 --- a/internal/db/migrations/000018_user_merges.down.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 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. - -DROP TABLE IF EXISTS user_merges; diff --git a/internal/db/migrations/000018_user_merges.up.sql b/internal/db/migrations/000018_user_merges.up.sql deleted file mode 100644 index 7aeeb8844..000000000 --- a/internal/db/migrations/000018_user_merges.up.sql +++ /dev/null @@ -1,30 +0,0 @@ --- 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. - -CREATE TABLE IF NOT EXISTS user_merges -( - id BIGINT NOT NULL AUTO_INCREMENT, - retiring_user_id VARCHAR(255) NOT NULL, - surviving_user_id VARCHAR(255) NOT NULL, - reason TEXT NULL, - merged_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), - PRIMARY KEY (id), - UNIQUE KEY uq_user_merges_retiring (retiring_user_id), - KEY idx_user_merges_surviving (surviving_user_id), - CONSTRAINT fk_user_merges_retiring FOREIGN KEY (retiring_user_id) REFERENCES users (id) ON DELETE RESTRICT, - CONSTRAINT fk_user_merges_surviving FOREIGN KEY (surviving_user_id) REFERENCES users (id) ON DELETE RESTRICT -) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; diff --git a/internal/server/server.go b/internal/server/server.go index 30e05a9a3..9844b8d3a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -58,8 +58,6 @@ func (s *Server) routes() { s.mux.HandleFunc("GET /users/{id}", s.getUser) s.mux.HandleFunc("PUT /users/{id}/status", s.updateUserStatus) s.mux.HandleFunc("POST /users/merge", s.mergeUsers) - s.mux.HandleFunc("GET /users/{id}/merge", s.getUserMergeByRetiringUser) - s.mux.HandleFunc("GET /users/{id}/merged-users", s.listUserMergesBySurvivingUser) s.mux.HandleFunc("POST /projects", s.createProject) s.mux.HandleFunc("GET /projects/{id}", s.getProject) @@ -146,11 +144,6 @@ func (s *Server) routes() { s.mux.HandleFunc("GET /user-identities/oidc-subjects/{oidcSub}", s.getUserIdentityByOIDCSub) s.mux.HandleFunc("GET /users/{id}/user-identities", s.listUserIdentitiesForUser) - s.mux.HandleFunc("POST /user-dns", s.addUserDN) - s.mux.HandleFunc("GET /user-dns/{id}", s.getUserDN) - s.mux.HandleFunc("DELETE /user-dns/{id}", s.removeUserDN) - s.mux.HandleFunc("GET /user-dns/lookup", s.getUserDNByDN) - s.mux.HandleFunc("GET /users/{id}/user-dns", s.listUserDNs) } func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) { @@ -1000,64 +993,9 @@ func (s *Server) deleteUserIdentity(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (s *Server) addUserDN(w http.ResponseWriter, r *http.Request) { - var d models.UserDN - if err := decodeJSON(r, &d); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - created, err := s.svc.AddUserDN(r.Context(), &d) - if err != nil { - writeServiceError(w, err) - return - } - writeJSON(w, http.StatusCreated, created) -} - -func (s *Server) getUserDN(w http.ResponseWriter, r *http.Request) { - d, err := s.svc.GetUserDN(r.Context(), r.PathValue("id")) - if err != nil { - writeServiceError(w, err) - return - } - writeJSON(w, http.StatusOK, d) -} - -func (s *Server) getUserDNByDN(w http.ResponseWriter, r *http.Request) { - dn := r.URL.Query().Get("dn") - if dn == "" { - writeError(w, http.StatusBadRequest, errors.New("dn query parameter is required")) - return - } - d, err := s.svc.GetUserDNByDN(r.Context(), dn) - if err != nil { - writeServiceError(w, err) - return - } - writeJSON(w, http.StatusOK, d) -} - -func (s *Server) listUserDNs(w http.ResponseWriter, r *http.Request) { - out, err := s.svc.ListUserDNs(r.Context(), r.PathValue("id")) - if err != nil { - writeServiceError(w, err) - return - } - writeJSON(w, http.StatusOK, out) -} - -func (s *Server) removeUserDN(w http.ResponseWriter, r *http.Request) { - if err := s.svc.RemoveUserDN(r.Context(), r.PathValue("id")); err != nil { - writeServiceError(w, err) - return - } - w.WriteHeader(http.StatusNoContent) -} - type mergeUsersRequest struct { SurvivingUserID string `json:"surviving_user_id"` RetiringUserID string `json:"retiring_user_id"` - Reason string `json:"reason,omitempty"` } func (s *Server) mergeUsers(w http.ResponseWriter, r *http.Request) { @@ -1066,7 +1004,7 @@ func (s *Server) mergeUsers(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, err) return } - survivor, err := s.svc.MergeUsers(r.Context(), req.SurvivingUserID, req.RetiringUserID, req.Reason) + survivor, err := s.svc.MergeUsers(r.Context(), req.SurvivingUserID, req.RetiringUserID) if err != nil { writeServiceError(w, err) return @@ -1074,24 +1012,6 @@ func (s *Server) mergeUsers(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, survivor) } -func (s *Server) getUserMergeByRetiringUser(w http.ResponseWriter, r *http.Request) { - m, err := s.svc.GetUserMergeByRetiringUser(r.Context(), r.PathValue("id")) - if err != nil { - writeServiceError(w, err) - return - } - writeJSON(w, http.StatusOK, m) -} - -func (s *Server) listUserMergesBySurvivingUser(w http.ResponseWriter, r *http.Request) { - out, err := s.svc.ListUserMergesBySurvivingUser(r.Context(), r.PathValue("id")) - if err != nil { - writeServiceError(w, err) - return - } - writeJSON(w, http.StatusOK, out) -} - // LoggingMiddleware logs every request once it completes. func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/store/store.go b/internal/store/store.go index 797bb5526..f3a7a6b00 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -43,17 +43,6 @@ type UserStore interface { Delete(ctx context.Context, tx *sql.Tx, id string) error } -// UserMergeStore records when one user is consolidated into another. Rows are -// append-only; each retiring user can be merged at most once. -type UserMergeStore interface { - // Record inserts a new merge record within the provided transaction. - Record(ctx context.Context, tx *sql.Tx, retiringUserID, survivingUserID, reason string) error - // FindByRetiringUser returns the merge record whose retiring user matches, or nil if absent. - FindByRetiringUser(ctx context.Context, retiringUserID string) (*models.UserMerge, error) - // FindBySurvivingUser returns every merge record whose survivor matches, oldest first. - FindBySurvivingUser(ctx context.Context, survivingUserID string) ([]models.UserMerge, error) -} - // OrganizationStore defines persistence operations for organizations. type OrganizationStore interface { // FindByID returns the organization with the given ID, or nil if not found. @@ -130,23 +119,6 @@ type UserIdentityStore interface { Delete(ctx context.Context, tx *sql.Tx, id string) error } -// UserDNStore defines persistence operations for X.509 distinguished-name -// bindings against a Custos user. -type UserDNStore interface { - // FindByID returns the DN binding with the given ID, or nil if not found. - FindByID(ctx context.Context, id string) (*models.UserDN, error) - // FindByDN returns the binding matching the given DN, or nil if absent. - FindByDN(ctx context.Context, dn string) (*models.UserDN, error) - // FindByUser returns every DN bound to the given user, ordered by created_at. - FindByUser(ctx context.Context, userID string) ([]models.UserDN, error) - // Create inserts a new DN binding within the provided transaction. - Create(ctx context.Context, tx *sql.Tx, d *models.UserDN) error - // ReassignUser moves every DN owned by fromUserID over to toUserID, dropping duplicates. - ReassignUser(ctx context.Context, tx *sql.Tx, fromUserID, toUserID string) error - // Delete removes a DN binding by ID within the provided transaction. - Delete(ctx context.Context, tx *sql.Tx, id string) error -} - // ProjectStore defines persistence operations for projects. type ProjectStore interface { // FindByID returns the project with the given ID, or nil if not found. diff --git a/internal/store/user_dn_store.go b/internal/store/user_dn_store.go deleted file mode 100644 index bfe6f69fe..000000000 --- a/internal/store/user_dn_store.go +++ /dev/null @@ -1,103 +0,0 @@ -// 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 store - -import ( - "context" - "database/sql" - "errors" - - "github.com/jmoiron/sqlx" - - "github.com/apache/airavata-custos/pkg/models" -) - -type mysqlUserDNStore struct { - db *sqlx.DB -} - -// NewUserDNStore returns a MySQL-backed UserDNStore. -func NewUserDNStore(db *sqlx.DB) UserDNStore { - return &mysqlUserDNStore{db: db} -} - -const userDNColumns = `id, user_id, dn, created_at` - -func (s *mysqlUserDNStore) FindByID(ctx context.Context, id string) (*models.UserDN, error) { - var d models.UserDN - err := s.db.GetContext(ctx, &d, - `SELECT `+userDNColumns+` FROM user_dns WHERE id = ?`, id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - return nil, err - } - return &d, nil -} - -func (s *mysqlUserDNStore) FindByDN(ctx context.Context, dn string) (*models.UserDN, error) { - var d models.UserDN - err := s.db.GetContext(ctx, &d, - `SELECT `+userDNColumns+` FROM user_dns WHERE dn = ?`, dn) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - return nil, err - } - return &d, nil -} - -func (s *mysqlUserDNStore) FindByUser(ctx context.Context, userID string) ([]models.UserDN, error) { - var out []models.UserDN - err := s.db.SelectContext(ctx, &out, - `SELECT `+userDNColumns+` FROM user_dns WHERE user_id = ? ORDER BY created_at ASC`, - userID) - if err != nil { - return nil, err - } - return out, nil -} - -func (s *mysqlUserDNStore) Create(ctx context.Context, tx *sql.Tx, d *models.UserDN) error { - _, err := tx.ExecContext(ctx, - `INSERT INTO user_dns (id, user_id, dn) VALUES (?, ?, ?)`, - d.ID, d.UserID, d.DN) - return err -} - -func (s *mysqlUserDNStore) ReassignUser(ctx context.Context, tx *sql.Tx, fromUserID, toUserID string) error { - // Drop fromUserID's DNs already held by the survivor, then move the rest. - if _, err := tx.ExecContext(ctx, - `DELETE FROM user_dns - WHERE user_id = ? - AND dn IN (SELECT dn FROM (SELECT dn FROM user_dns WHERE user_id = ?) AS s)`, - fromUserID, toUserID); err != nil { - return err - } - _, err := tx.ExecContext(ctx, - `UPDATE user_dns SET user_id = ? WHERE user_id = ?`, - toUserID, fromUserID) - return err -} - -func (s *mysqlUserDNStore) Delete(ctx context.Context, tx *sql.Tx, id string) error { - _, err := tx.ExecContext(ctx, `DELETE FROM user_dns WHERE id = ?`, id) - return err -} diff --git a/internal/store/user_merge_store.go b/internal/store/user_merge_store.go deleted file mode 100644 index 827462e55..000000000 --- a/internal/store/user_merge_store.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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 store - -import ( - "context" - "database/sql" - "errors" - - "github.com/jmoiron/sqlx" - - "github.com/apache/airavata-custos/pkg/models" -) - -type mysqlUserMergeStore struct { - db *sqlx.DB -} - -// NewUserMergeStore returns a MySQL-backed UserMergeStore. -func NewUserMergeStore(db *sqlx.DB) UserMergeStore { - return &mysqlUserMergeStore{db: db} -} - -const userMergeColumns = `id, retiring_user_id, surviving_user_id, COALESCE(reason, '') AS reason, merged_at` - -func (s *mysqlUserMergeStore) Record(ctx context.Context, tx *sql.Tx, retiringUserID, survivingUserID, reason string) error { - var reasonArg any - if reason == "" { - reasonArg = nil - } else { - reasonArg = reason - } - _, err := tx.ExecContext(ctx, - `INSERT INTO user_merges (retiring_user_id, surviving_user_id, reason) - VALUES (?, ?, ?)`, - retiringUserID, survivingUserID, reasonArg) - return err -} - -func (s *mysqlUserMergeStore) FindByRetiringUser(ctx context.Context, retiringUserID string) (*models.UserMerge, error) { - var m models.UserMerge - err := s.db.GetContext(ctx, &m, - `SELECT `+userMergeColumns+` FROM user_merges WHERE retiring_user_id = ?`, retiringUserID) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - return nil, err - } - return &m, nil -} - -func (s *mysqlUserMergeStore) FindBySurvivingUser(ctx context.Context, survivingUserID string) ([]models.UserMerge, error) { - var out []models.UserMerge - err := s.db.SelectContext(ctx, &out, - `SELECT `+userMergeColumns+` FROM user_merges WHERE surviving_user_id = ? ORDER BY merged_at ASC`, - survivingUserID) - if err != nil { - return nil, err - } - return out, nil -} diff --git a/pkg/events/types.go b/pkg/events/types.go index 6cfc6300a..4110782c6 100644 --- a/pkg/events/types.go +++ b/pkg/events/types.go @@ -117,13 +117,6 @@ const ( UserIdentityDeleteEvent EventType = "user_identity::delete" ) -// UserDN lifecycle message types. DN bindings are append-only credentials, so -// no update topic. -const ( - UserDNCreateEvent EventType = "user_dn::create" - UserDNDeleteEvent EventType = "user_dn::delete" -) - // Event represents a change in the system that downstream consumers may be interested in. // The payload is the full record after the change (e.g. the // new state of a project after an update). diff --git a/pkg/events/user_dn_subscribe.go b/pkg/events/user_dn_subscribe.go deleted file mode 100644 index 9190976e6..000000000 --- a/pkg/events/user_dn_subscribe.go +++ /dev/null @@ -1,57 +0,0 @@ -// 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 events - -import ( - "log/slog" - - "github.com/apache/airavata-custos/pkg/models" -) - -// UserDNHandler handles DN-binding lifecycle events with a typed payload. -type UserDNHandler func(dn models.UserDN) - -// SubscribeUserDNCreated registers a typed handler invoked whenever a -// user_dn::create event is published. -func (b *Bus) SubscribeUserDNCreated(handler UserDNHandler) { - b.subscribeUserDN(UserDNCreateEvent, handler) -} - -// SubscribeUserDNDeleted registers a typed handler invoked whenever a -// user_dn::delete event is published. -func (b *Bus) SubscribeUserDNDeleted(handler UserDNHandler) { - b.subscribeUserDN(UserDNDeleteEvent, handler) -} - -func (b *Bus) subscribeUserDN(topic EventType, handler UserDNHandler) { - b.Subscribe(topic, func(event Event, value interface{}) { - switch d := value.(type) { - case models.UserDN: - handler(d) - case *models.UserDN: - if d != nil { - handler(*d) - } - default: - slog.Warn("user dn event payload has unexpected type", - "type", event.Type, - "got", value, - ) - } - }) -} diff --git a/pkg/models/identity.go b/pkg/models/identity.go index 83d24257b..149f0e855 100644 --- a/pkg/models/identity.go +++ b/pkg/models/identity.go @@ -33,13 +33,3 @@ type UserIdentity struct { Metadata string `json:"metadata,omitempty" db:"metadata"` // JSON-encoded source-specific fields CreatedAt time.Time `json:"created_at" db:"created_at"` } - -// UserDN binds an X.509 distinguished name (e.g. mTLS client cert subject) to -// a User. Append-only: DNs are credentials and are added or removed, never -// edited. -type UserDN struct { - ID string `json:"id" db:"id"` - UserID string `json:"user_id" db:"user_id"` - DN string `json:"dn" db:"dn"` - CreatedAt time.Time `json:"created_at" db:"created_at"` -} diff --git a/pkg/models/project.go b/pkg/models/project.go index 38de362a0..f73ec63de 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -46,14 +46,3 @@ type User struct { Email string `json:"email" db:"email"` Status UserStatus `json:"status" db:"status"` } - -// UserMerge is the audit record that links a retiring user to the surviving -// user that absorbed its identity-forward state. Each retiring user can be -// merged at most once; merges are not reversed in-place. -type UserMerge struct { - ID int64 `json:"id" db:"id"` - RetiringUserID string `json:"retiring_user_id" db:"retiring_user_id"` - SurvivingUserID string `json:"surviving_user_id" db:"surviving_user_id"` - Reason string `json:"reason,omitempty" db:"reason"` - MergedAt time.Time `json:"merged_at" db:"merged_at"` -} diff --git a/pkg/service/service.go b/pkg/service/service.go index 6b0cdb555..0a1734132 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -52,8 +52,6 @@ type Service struct { membershipOverrides store.ComputeAllocationMembershipResourceOverrideStore usages store.ComputeAllocationUsageStore userIdentities store.UserIdentityStore - userDNs store.UserDNStore - userMerges store.UserMergeStore } // New constructs a Service backed by the supplied database handle. @@ -78,8 +76,6 @@ func New(database *sqlx.DB, eventBus *events.Bus) *Service { membershipOverrides: store.NewComputeAllocationMembershipResourceOverrideStore(database), usages: store.NewComputeAllocationUsageStore(database), userIdentities: store.NewUserIdentityStore(database), - userDNs: store.NewUserDNStore(database), - userMerges: store.NewUserMergeStore(database), } } @@ -105,8 +101,6 @@ func NewWithStores( memberships store.ComputeAllocationMembershipStore, usages store.ComputeAllocationUsageStore, userIdentities store.UserIdentityStore, - userDNs store.UserDNStore, - userMerges store.UserMergeStore, ) *Service { return &Service{ db: database, @@ -127,8 +121,6 @@ func NewWithStores( memberships: memberships, usages: usages, userIdentities: userIdentities, - userDNs: userDNs, - userMerges: userMerges, } } diff --git a/pkg/service/user_dn.go b/pkg/service/user_dn.go deleted file mode 100644 index 611b93f9d..000000000 --- a/pkg/service/user_dn.go +++ /dev/null @@ -1,127 +0,0 @@ -// 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 service - -import ( - "context" - "database/sql" - "fmt" - - "github.com/apache/airavata-custos/pkg/events" - "github.com/apache/airavata-custos/pkg/models" -) - -// AddUserDN binds a DN to a user. If d.ID is empty, a new UUID is generated. -// The referenced user must already exist; (user_id, dn) is unique. -func (s *Service) AddUserDN(ctx context.Context, d *models.UserDN) (*models.UserDN, error) { - if d == nil { - return nil, fmt.Errorf("%w: user dn is nil", ErrInvalidInput) - } - if d.UserID == "" { - return nil, fmt.Errorf("%w: user dn user_id is required", ErrInvalidInput) - } - if d.DN == "" { - return nil, fmt.Errorf("%w: user dn dn is required", ErrInvalidInput) - } - - if user, err := s.users.FindByID(ctx, d.UserID); err != nil { - return nil, fmt.Errorf("verify user: %w", err) - } else if user == nil { - return nil, fmt.Errorf("%w: user %q does not exist", ErrInvalidInput, d.UserID) - } - - if existing, err := s.userDNs.FindByDN(ctx, d.DN); err != nil { - return nil, fmt.Errorf("lookup user dn: %w", err) - } else if existing != nil { - return nil, fmt.Errorf("%w: dn %q", ErrAlreadyExists, d.DN) - } - - if d.ID == "" { - d.ID = newID() - } - - if err := s.inTx(ctx, func(tx *sql.Tx) error { - return s.userDNs.Create(ctx, tx, d) - }); err != nil { - return nil, fmt.Errorf("add user dn: %w", err) - } - - s.eventBus.Publish(events.UserDNCreateEvent, d) - return d, nil -} - -// GetUserDN retrieves a DN binding by ID. Returns ErrNotFound when no row matches. -func (s *Service) GetUserDN(ctx context.Context, id string) (*models.UserDN, error) { - d, err := s.userDNs.FindByID(ctx, id) - if err != nil { - return nil, fmt.Errorf("get user dn: %w", err) - } - if d == nil { - return nil, ErrNotFound - } - return d, nil -} - -// GetUserDNByDN performs a reverse lookup from DN to binding. -func (s *Service) GetUserDNByDN(ctx context.Context, dn string) (*models.UserDN, error) { - if dn == "" { - return nil, fmt.Errorf("%w: dn is required", ErrInvalidInput) - } - d, err := s.userDNs.FindByDN(ctx, dn) - if err != nil { - return nil, fmt.Errorf("get user dn by dn: %w", err) - } - if d == nil { - return nil, ErrNotFound - } - return d, nil -} - -// ListUserDNs returns every DN bound to the given user. -func (s *Service) ListUserDNs(ctx context.Context, userID string) ([]models.UserDN, error) { - if userID == "" { - return nil, fmt.Errorf("%w: user_id is required", ErrInvalidInput) - } - out, err := s.userDNs.FindByUser(ctx, userID) - if err != nil { - return nil, fmt.Errorf("list user dns: %w", err) - } - return out, nil -} - -// RemoveUserDN removes a DN binding by ID. -func (s *Service) RemoveUserDN(ctx context.Context, id string) error { - if id == "" { - return fmt.Errorf("%w: user dn id is required", ErrInvalidInput) - } - d, err := s.userDNs.FindByID(ctx, id) - if err != nil { - return fmt.Errorf("lookup user dn: %w", err) - } - if d == nil { - return ErrNotFound - } - if err := s.inTx(ctx, func(tx *sql.Tx) error { - return s.userDNs.Delete(ctx, tx, id) - }); err != nil { - return fmt.Errorf("remove user dn: %w", err) - } - - s.eventBus.Publish(events.UserDNDeleteEvent, d) - return nil -} diff --git a/pkg/service/user_merge.go b/pkg/service/user_merge.go index 3d8860e02..8a07b2ee1 100644 --- a/pkg/service/user_merge.go +++ b/pkg/service/user_merge.go @@ -28,13 +28,11 @@ import ( // MergeUsers consolidates the retiring user into the surviving user. All // identity-forward state moves to the survivor; historical truth stays in -// place. The retiring user is flipped to status=MERGED and a row is written -// to user_merges with the surviving user and the given reason. All work -// happens in a single transaction. +// place. The retiring user is flipped to status=MERGED. All work happens in a +// single transaction. // // Moved to survivor (duplicates on the retiring user are dropped first): // - user_identities -// - user_dns // - compute_cluster_users // - projects.project_pi_id // - compute_allocation_memberships @@ -42,7 +40,11 @@ import ( // Left in place (who actually did the thing): // - compute_allocation_change_requests (requester / approver) // - compute_allocation_usages -func (s *Service) MergeUsers(ctx context.Context, survivingID, retiringID, reason string) (*models.User, error) { +// +// Not idempotent — a retiring user already in status=MERGED is rejected with +// ErrAlreadyExists. Callers that need replay safety should fetch the retiring +// user by ID and skip the call when its status is already MERGED. +func (s *Service) MergeUsers(ctx context.Context, survivingID, retiringID string) (*models.User, error) { if survivingID == "" || retiringID == "" { return nil, fmt.Errorf("%w: surviving and retiring user IDs are required", ErrInvalidInput) } @@ -67,30 +69,14 @@ func (s *Service) MergeUsers(ctx context.Context, survivingID, retiringID, reaso if retiring == nil { return nil, fmt.Errorf("%w: retiring user %q does not exist", ErrInvalidInput, retiringID) } - - // Idempotency: re-running the same merge is a no-op; merging the same - // retiring user into a different survivor is rejected. if retiring.Status == models.UserMerged { - prior, err := s.userMerges.FindByRetiringUser(ctx, retiringID) - if err != nil { - return nil, fmt.Errorf("lookup prior merge: %w", err) - } - if prior != nil { - if prior.SurvivingUserID == survivingID { - return survivor, nil - } - return nil, fmt.Errorf("%w: user %q already merged into %q", - ErrAlreadyExists, retiringID, prior.SurvivingUserID) - } + return nil, fmt.Errorf("%w: retiring user %q is already merged", ErrAlreadyExists, retiringID) } if err := s.inTx(ctx, func(tx *sql.Tx) error { if err := s.userIdentities.ReassignUser(ctx, tx, retiringID, survivingID); err != nil { return fmt.Errorf("reassign user identities: %w", err) } - if err := s.userDNs.ReassignUser(ctx, tx, retiringID, survivingID); err != nil { - return fmt.Errorf("reassign user dns: %w", err) - } if err := s.clusterUsers.ReassignUser(ctx, tx, retiringID, survivingID); err != nil { return fmt.Errorf("reassign compute cluster users: %w", err) } @@ -100,10 +86,7 @@ func (s *Service) MergeUsers(ctx context.Context, survivingID, retiringID, reaso if err := s.memberships.ReassignUser(ctx, tx, retiringID, survivingID); err != nil { return fmt.Errorf("reassign memberships: %w", err) } - if err := s.users.UpdateStatus(ctx, tx, retiringID, models.UserMerged); err != nil { - return fmt.Errorf("mark retiring user merged: %w", err) - } - return s.userMerges.Record(ctx, tx, retiringID, survivingID, reason) + return s.users.UpdateStatus(ctx, tx, retiringID, models.UserMerged) }); err != nil { return nil, fmt.Errorf("merge users: %w", err) } @@ -113,32 +96,3 @@ func (s *Service) MergeUsers(ctx context.Context, survivingID, retiringID, reaso s.eventBus.Publish(events.UserUpdateEvent, survivor) return survivor, nil } - -// GetUserMergeByRetiringUser returns the merge record for a retiring user, or -// ErrNotFound if the user has not been merged. -func (s *Service) GetUserMergeByRetiringUser(ctx context.Context, retiringUserID string) (*models.UserMerge, error) { - if retiringUserID == "" { - return nil, fmt.Errorf("%w: retiring_user_id is required", ErrInvalidInput) - } - m, err := s.userMerges.FindByRetiringUser(ctx, retiringUserID) - if err != nil { - return nil, fmt.Errorf("get user merge: %w", err) - } - if m == nil { - return nil, ErrNotFound - } - return m, nil -} - -// ListUserMergesBySurvivingUser returns every merge record absorbed by the -// given surviving user, oldest first. -func (s *Service) ListUserMergesBySurvivingUser(ctx context.Context, survivingUserID string) ([]models.UserMerge, error) { - if survivingUserID == "" { - return nil, fmt.Errorf("%w: surviving_user_id is required", ErrInvalidInput) - } - out, err := s.userMerges.FindBySurvivingUser(ctx, survivingUserID) - if err != nil { - return nil, fmt.Errorf("list user merges by surviving user: %w", err) - } - return out, nil -}
