From 2e8fde35bf0f93bb6581472be055b7b431b0f9a2 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 27 Mar 2025 11:24:23 +0100 Subject: [PATCH 1/4] feat(users): Store user access on user's table Signed-off-by: Javier Rodriguez --- app/controlplane/cmd/main.go | 14 +- app/controlplane/cmd/wire.go | 5 + app/controlplane/cmd/wire_gen.go | 8 +- .../usercontext/allowlist_middleware.go | 40 +--- app/controlplane/pkg/biz/biz.go | 1 + app/controlplane/pkg/biz/user.go | 6 + app/controlplane/pkg/biz/useraccess_syncer.go | 188 ++++++++++++++++++ .../ent/migrate/migrations/20250327094547.sql | 4 + .../pkg/data/ent/migrate/migrations/atlas.sum | 3 +- .../pkg/data/ent/migrate/schema.go | 11 + app/controlplane/pkg/data/ent/mutation.go | 80 ++++++-- app/controlplane/pkg/data/ent/runtime.go | 4 + app/controlplane/pkg/data/ent/schema-viz.html | 2 +- app/controlplane/pkg/data/ent/schema/user.go | 11 + app/controlplane/pkg/data/ent/user.go | 13 ++ app/controlplane/pkg/data/ent/user/user.go | 10 + app/controlplane/pkg/data/ent/user/where.go | 15 ++ app/controlplane/pkg/data/ent/user_create.go | 65 ++++++ app/controlplane/pkg/data/ent/user_update.go | 34 ++++ app/controlplane/pkg/data/user.go | 64 ++++++ 20 files changed, 523 insertions(+), 55 deletions(-) create mode 100644 app/controlplane/pkg/biz/useraccess_syncer.go create mode 100644 app/controlplane/pkg/data/ent/migrate/migrations/20250327094547.sql diff --git a/app/controlplane/cmd/main.go b/app/controlplane/cmd/main.go index b38ae0611..8db003d70 100644 --- a/app/controlplane/cmd/main.go +++ b/app/controlplane/cmd/main.go @@ -62,7 +62,9 @@ func init() { flag.StringVar(&flagconf, "conf", "../configs", "config path, eg: -conf config.yaml") } -func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTPMetricsServer, profilerSvc *server.HTTPProfilerServer, expirer *biz.WorkflowRunExpirerUseCase, plugins sdk.AvailablePlugins, tokenSync *biz.APITokenSyncerUseCase, cfg *conf.Bootstrap) *app { +func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTPMetricsServer, profilerSvc *server.HTTPProfilerServer, + expirer *biz.WorkflowRunExpirerUseCase, plugins sdk.AvailablePlugins, tokenSync *biz.APITokenSyncerUseCase, + userAccessSyncer *biz.UserAccessSyncerUseCase, cfg *conf.Bootstrap) *app { servers := []transport.Server{gs, hs, ms} if cfg.EnableProfiler { servers = append(servers, profilerSvc) @@ -76,7 +78,7 @@ func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTP kratos.Metadata(map[string]string{}), kratos.Logger(logger), kratos.Server(servers...), - ), expirer, plugins, tokenSync} + ), expirer, plugins, tokenSync, userAccessSyncer} } func main() { @@ -158,6 +160,13 @@ func main() { } }() + // Sync user access + go func() { + if err := app.userAccessSyncer.StartSyncingUserAccess(ctx); err != nil { + _ = logger.Log(log.LevelError, "msg", "syncing user access", "error", err) + } + }() + // start and wait for stop signal if err := app.Run(); err != nil { panic(err) @@ -170,6 +179,7 @@ type app struct { runsExpirer *biz.WorkflowRunExpirerUseCase availablePlugins sdk.AvailablePlugins tokenAuthSyncer *biz.APITokenSyncerUseCase + userAccessSyncer *biz.UserAccessSyncerUseCase } // Connection to nats is optional, if not configured, pubsub will be disabled diff --git a/app/controlplane/cmd/wire.go b/app/controlplane/cmd/wire.go index 0c834ec3c..48c548aec 100644 --- a/app/controlplane/cmd/wire.go +++ b/app/controlplane/cmd/wire.go @@ -61,6 +61,7 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger, sdk.Availabl newNatsConnection, auditor.NewAuditLogPublisher, newCASServerOptions, + newAuthAllowList, ), ) } @@ -95,3 +96,7 @@ func newCASServerOptions(in *conf.Bootstrap_CASServer) *biz.CASServerDefaultOpts DefaultEntryMaxSize: in.GetDefaultEntryMaxSize(), } } + +func newAuthAllowList(conf *conf.Bootstrap) *conf.Auth_AllowList { + return conf.Auth.GetAllowList() +} diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 368aa0899..8b8703f93 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -287,7 +287,9 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l } workflowRunExpirerUseCase := biz.NewWorkflowRunExpirerUseCase(workflowRunRepo, prometheusUseCase, logger) apiTokenSyncerUseCase := biz.NewAPITokenSyncerUseCase(apiTokenUseCase) - mainApp := newApp(logger, grpcServer, httpServer, httpMetricsServer, httpProfilerServer, workflowRunExpirerUseCase, availablePlugins, apiTokenSyncerUseCase, bootstrap) + auth_AllowList := newAuthAllowList(bootstrap) + userAccessSyncerUseCase := biz.NewUserAccessSyncerUseCase(logger, userRepo, auth_AllowList) + mainApp := newApp(logger, grpcServer, httpServer, httpMetricsServer, httpProfilerServer, workflowRunExpirerUseCase, availablePlugins, apiTokenSyncerUseCase, userAccessSyncerUseCase, bootstrap) return mainApp, func() { cleanup() }, nil @@ -327,3 +329,7 @@ func newCASServerOptions(in *conf.Bootstrap_CASServer) *biz.CASServerDefaultOpts DefaultEntryMaxSize: in.GetDefaultEntryMaxSize(), } } + +func newAuthAllowList(conf2 *conf.Bootstrap) *conf.Auth_AllowList { + return conf2.Auth.GetAllowList() +} diff --git a/app/controlplane/internal/usercontext/allowlist_middleware.go b/app/controlplane/internal/usercontext/allowlist_middleware.go index 72b0fc41e..0c5f21beb 100644 --- a/app/controlplane/internal/usercontext/allowlist_middleware.go +++ b/app/controlplane/internal/usercontext/allowlist_middleware.go @@ -18,11 +18,12 @@ package usercontext import ( "context" "fmt" - "strings" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" + "github.com/go-kratos/kratos/v2/middleware" "github.com/go-kratos/kratos/v2/transport" ) @@ -53,7 +54,7 @@ func CheckUserInAllowList(allowList *conf.Auth_AllowList) middleware.Middleware } // If there are not items in the allowList we allow all users - allow, err := inAllowList(allowList.GetRules(), user.Email) + allow, err := biz.UserEmailInAllowlist(allowList.GetRules(), user.Email) if err != nil { return nil, v1.ErrorAllowListErrorNotInList("error checking user in allowList: %v", err) } @@ -83,38 +84,3 @@ func selectedRoute(ctx context.Context, selectedRoutes []string) bool { return false } - -func inAllowList(allowList []string, email string) (bool, error) { - for _, allowListEntry := range allowList { - // it's a direct email match - if allowListEntry == email { - return true, nil - } - - // Check if the entry is a domain and the email is part of it - // extract the domain from the allowList entry - // i.e if the entry is @cyberdyne.io, we get cyberdyne.io - domainComponent := strings.Split(allowListEntry, "@") - if len(domainComponent) != 2 { - return false, fmt.Errorf("invalid domain entry: %q", allowListEntry) - } - - // it's not a domain since it contains an username, then continue - if domainComponent[0] != "" { - continue - } - - // Compare the domains - emailComponents := strings.Split(email, "@") - if len(emailComponents) != 2 { - return false, fmt.Errorf("invalid email: %q", email) - } - - // check if against a potential domain entry in the allowList - if emailComponents[1] == domainComponent[1] { - return true, nil - } - } - - return false, nil -} diff --git a/app/controlplane/pkg/biz/biz.go b/app/controlplane/pkg/biz/biz.go index adaf3fa01..d4a9fb9ad 100644 --- a/app/controlplane/pkg/biz/biz.go +++ b/app/controlplane/pkg/biz/biz.go @@ -53,6 +53,7 @@ var ProviderSet = wire.NewSet( NewProjectVersionUseCase, NewProjectsUseCase, NewAuditorUseCase, + NewUserAccessSyncerUseCase, wire.Bind(new(PromObservable), new(*PrometheusUseCase)), wire.Struct(new(NewIntegrationUseCaseOpts), "*"), wire.Struct(new(NewUserUseCaseParams), "*"), diff --git a/app/controlplane/pkg/biz/user.go b/app/controlplane/pkg/biz/user.go index afe8cc9f8..e224a58d8 100644 --- a/app/controlplane/pkg/biz/user.go +++ b/app/controlplane/pkg/biz/user.go @@ -26,6 +26,8 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" config "github.com/chainloop-dev/chainloop/app/controlplane/pkg/conf/controlplane/config/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" + "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" ) @@ -41,6 +43,10 @@ type UserRepo interface { FindByEmail(ctx context.Context, email string) (*User, error) FindByID(ctx context.Context, userID uuid.UUID) (*User, error) Delete(ctx context.Context, userID uuid.UUID) error + FindAll(ctx context.Context, pagination *pagination.OffsetPaginationOpts) ([]*User, int, error) + UpdateAccess(ctx context.Context, userID uuid.UUID, isAccessRestricted bool) error + CountUsersWithRestrictedAccess(ctx context.Context) (int, error) + UpdateAllUsersAccess(ctx context.Context, isAccessRestricted bool) error } type UserOrgFinder interface { diff --git a/app/controlplane/pkg/biz/useraccess_syncer.go b/app/controlplane/pkg/biz/useraccess_syncer.go new file mode 100644 index 000000000..40d1141df --- /dev/null +++ b/app/controlplane/pkg/biz/useraccess_syncer.go @@ -0,0 +1,188 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed 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 biz + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/go-kratos/kratos/v2/log" + + conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" + "github.com/google/uuid" +) + +type UserAccessSyncerUseCase struct { + logger *log.Helper + // Repositories + userRepo UserRepo + // Configuration + allowList *conf.Auth_AllowList +} + +func NewUserAccessSyncerUseCase(logger log.Logger, userRepo UserRepo, allowList *conf.Auth_AllowList) *UserAccessSyncerUseCase { + return &UserAccessSyncerUseCase{ + userRepo: userRepo, + allowList: allowList, + logger: log.NewHelper(log.With(logger, "component", "biz/user_access_syncer")), + } +} + +// StartSyncingUserAccess starts syncing the access restriction status of all users based on the allowlist +func (u *UserAccessSyncerUseCase) StartSyncingUserAccess(ctx context.Context) error { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + u.logger.Infow("msg", "stopping user access sync") + return nil + case <-ticker.C: + u.logger.Infow("msg", "Syncing user access") + + // Count the number of users with restricted access + usersWithRestrictedAccess, err := u.userRepo.CountUsersWithRestrictedAccess(ctx) + if err != nil { + return fmt.Errorf("count users with restricted access: %w", err) + } + + // Update the access restriction status of all users based on the allowlist + if err := u.updateUserAccessBasedOnAllowList(ctx, usersWithRestrictedAccess); err != nil { + return fmt.Errorf("update user access based on allow list: %w", err) + } + + u.logger.Infow("msg", "User access synced") + } + } +} + +// updateUserAccessBasedOnAllowList updates the access restriction status of all users based on the allowlist +func (u *UserAccessSyncerUseCase) updateUserAccessBasedOnAllowList(ctx context.Context, usersWithRestrictedAccess int) error { + // If the allowlist is empty and there are users with restricted access, give access to those users + if u.allowList != nil && len(u.allowList.GetRules()) == 0 && usersWithRestrictedAccess > 0 { + if err := u.userRepo.UpdateAllUsersAccess(ctx, false); err != nil { + return fmt.Errorf("update all users access: %w", err) + } + } else { + // Sync the access restriction status of all users based on the allowlist + if err := u.syncUserAccess(ctx); err != nil { + return fmt.Errorf("sync user access: %w", err) + } + } + + return nil +} + +// syncUserAccess syncs the access restriction status of all users based on the allowlist +func (u *UserAccessSyncerUseCase) syncUserAccess(ctx context.Context) error { + var ( + offset = 1 + limit = 50 + ) + + for { + pgOpts, err := pagination.NewOffsetPaginationOpts(offset, limit) + if err != nil { + return fmt.Errorf("failed to create pagination options: %w", err) + } + + users, _, err := u.userRepo.FindAll(ctx, pgOpts) + if err != nil { + return fmt.Errorf("failed to list users: %w", err) + } + + // If the allowlist is empty, we deactivate the access restriction for all users + isAllowListDeactivated := u.allowList == nil || len(u.allowList.GetRules()) == 0 + + for _, user := range users { + if err := u.updateUserAccessRestriction(ctx, user, isAllowListDeactivated); err != nil { + return fmt.Errorf("failed to update user access: %w", err) + } + } + + if len(users) < limit { + break + } + + offset++ + } + + return nil +} + +// updateUserAccessRestriction updates the access restriction status of a user +func (u *UserAccessSyncerUseCase) updateUserAccessRestriction(ctx context.Context, user *User, isAllowListDeactivated bool) error { + allow, err := UserEmailInAllowlist(u.allowList.GetRules(), user.Email) + if err != nil { + return fmt.Errorf("error checking user in allowList: %w", err) + } + + isAccessRestricted := !allow + if isAllowListDeactivated { + isAccessRestricted = false + } + + parsedUserUUID, err := uuid.Parse(user.ID) + if err != nil { + return fmt.Errorf("invalid user ID: %w", err) + } + + if err := u.userRepo.UpdateAccess(ctx, parsedUserUUID, isAccessRestricted); err != nil { + return fmt.Errorf("failed to update user access: %w", err) + } + + return nil +} + +// UserEmailInAllowlist checks if the user email is in the allowlist +func UserEmailInAllowlist(allowList []string, email string) (bool, error) { + for _, allowListEntry := range allowList { + // it's a direct email match + if allowListEntry == email { + return true, nil + } + + // Check if the entry is a domain and the email is part of it + // extract the domain from the allowList entry + // i.e if the entry is @cyberdyne.io, we get cyberdyne.io + domainComponent := strings.Split(allowListEntry, "@") + if len(domainComponent) != 2 { + return false, fmt.Errorf("invalid domain entry: %q", allowListEntry) + } + + // it's not a domain since it contains an username, then continue + if domainComponent[0] != "" { + continue + } + + // Compare the domains + emailComponents := strings.Split(email, "@") + if len(emailComponents) != 2 { + return false, fmt.Errorf("invalid email: %q", email) + } + + // check if against a potential domain entry in the allowList + if emailComponents[1] == domainComponent[1] { + return true, nil + } + } + + return false, nil +} diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250327094547.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250327094547.sql new file mode 100644 index 000000000..c722a95e5 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250327094547.sql @@ -0,0 +1,4 @@ +-- Modify "users" table +ALTER TABLE "users" ADD COLUMN "has_restricted_access" boolean NOT NULL DEFAULT true; +-- Create index "user_has_restricted_access" to table: "users" +CREATE INDEX "user_has_restricted_access" ON "users" ("has_restricted_access") WHERE (has_restricted_access IS TRUE); diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 95613d38e..8d83aa971 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:YVPucsMOwyVrKFmOgfjfQ4xLEyqpppOJ7CNEXsVjiTo= +h1:FURGzv6iR93WTD2fkOT2M1QnDf5bRjulR0oU5ygDzh8= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -79,3 +79,4 @@ h1:YVPucsMOwyVrKFmOgfjfQ4xLEyqpppOJ7CNEXsVjiTo= 20250203084822.sql h1:xKQ2szI/uaQjz9mOthOE5SFO8wV/maJglRfciXjb+P8= 20250303153626.sql h1:y38iNqTO+lutsb2hPu+gepPDgSDmsTcWbgu7kMpbIzE= 20250326110627.sql h1:kTneMHSqpE7I8Gl88jjTy2olXpdg/np0yA45lqIxBic= +20250327094547.sql h1:qSl1MtpeZWr2EnDk83X7VmglDVF/Oyp8FkRhDHB1Lsw= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index f7ca11774..eb3bc7ecd 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -423,12 +423,23 @@ var ( {Name: "id", Type: field.TypeUUID, Unique: true}, {Name: "email", Type: field.TypeString, Unique: true}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, + {Name: "has_restricted_access", Type: field.TypeBool, Default: true}, } // UsersTable holds the schema information for the "users" table. UsersTable = &schema.Table{ Name: "users", Columns: UsersColumns, PrimaryKey: []*schema.Column{UsersColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "user_has_restricted_access", + Unique: false, + Columns: []*schema.Column{UsersColumns[3]}, + Annotation: &entsql.IndexAnnotation{ + Where: "has_restricted_access IS true", + }, + }, + }, } // WorkflowsColumns holds the columns for the "workflows" table. WorkflowsColumns = []*schema.Column{ diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index abdd47ccc..c80efca92 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -10141,18 +10141,19 @@ func (m *RobotAccountMutation) ResetEdge(name string) error { // UserMutation represents an operation that mutates the User nodes in the graph. type UserMutation struct { config - op Op - typ string - id *uuid.UUID - email *string - created_at *time.Time - clearedFields map[string]struct{} - memberships map[uuid.UUID]struct{} - removedmemberships map[uuid.UUID]struct{} - clearedmemberships bool - done bool - oldValue func(context.Context) (*User, error) - predicates []predicate.User + op Op + typ string + id *uuid.UUID + email *string + created_at *time.Time + has_restricted_access *bool + clearedFields map[string]struct{} + memberships map[uuid.UUID]struct{} + removedmemberships map[uuid.UUID]struct{} + clearedmemberships bool + done bool + oldValue func(context.Context) (*User, error) + predicates []predicate.User } var _ ent.Mutation = (*UserMutation)(nil) @@ -10331,6 +10332,42 @@ func (m *UserMutation) ResetCreatedAt() { m.created_at = nil } +// SetHasRestrictedAccess sets the "has_restricted_access" field. +func (m *UserMutation) SetHasRestrictedAccess(b bool) { + m.has_restricted_access = &b +} + +// HasRestrictedAccess returns the value of the "has_restricted_access" field in the mutation. +func (m *UserMutation) HasRestrictedAccess() (r bool, exists bool) { + v := m.has_restricted_access + if v == nil { + return + } + return *v, true +} + +// OldHasRestrictedAccess returns the old "has_restricted_access" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldHasRestrictedAccess(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldHasRestrictedAccess is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldHasRestrictedAccess requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldHasRestrictedAccess: %w", err) + } + return oldValue.HasRestrictedAccess, nil +} + +// ResetHasRestrictedAccess resets all changes to the "has_restricted_access" field. +func (m *UserMutation) ResetHasRestrictedAccess() { + m.has_restricted_access = nil +} + // AddMembershipIDs adds the "memberships" edge to the Membership entity by ids. func (m *UserMutation) AddMembershipIDs(ids ...uuid.UUID) { if m.memberships == nil { @@ -10419,13 +10456,16 @@ func (m *UserMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *UserMutation) Fields() []string { - fields := make([]string, 0, 2) + fields := make([]string, 0, 3) if m.email != nil { fields = append(fields, user.FieldEmail) } if m.created_at != nil { fields = append(fields, user.FieldCreatedAt) } + if m.has_restricted_access != nil { + fields = append(fields, user.FieldHasRestrictedAccess) + } return fields } @@ -10438,6 +10478,8 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) { return m.Email() case user.FieldCreatedAt: return m.CreatedAt() + case user.FieldHasRestrictedAccess: + return m.HasRestrictedAccess() } return nil, false } @@ -10451,6 +10493,8 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er return m.OldEmail(ctx) case user.FieldCreatedAt: return m.OldCreatedAt(ctx) + case user.FieldHasRestrictedAccess: + return m.OldHasRestrictedAccess(ctx) } return nil, fmt.Errorf("unknown User field %s", name) } @@ -10474,6 +10518,13 @@ func (m *UserMutation) SetField(name string, value ent.Value) error { } m.SetCreatedAt(v) return nil + case user.FieldHasRestrictedAccess: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetHasRestrictedAccess(v) + return nil } return fmt.Errorf("unknown User field %s", name) } @@ -10529,6 +10580,9 @@ func (m *UserMutation) ResetField(name string) error { case user.FieldCreatedAt: m.ResetCreatedAt() return nil + case user.FieldHasRestrictedAccess: + m.ResetHasRestrictedAccess() + return nil } return fmt.Errorf("unknown User field %s", name) } diff --git a/app/controlplane/pkg/data/ent/runtime.go b/app/controlplane/pkg/data/ent/runtime.go index 3a3b6b834..a90ce459e 100644 --- a/app/controlplane/pkg/data/ent/runtime.go +++ b/app/controlplane/pkg/data/ent/runtime.go @@ -217,6 +217,10 @@ func init() { userDescCreatedAt := userFields[2].Descriptor() // user.DefaultCreatedAt holds the default value on creation for the created_at field. user.DefaultCreatedAt = userDescCreatedAt.Default.(func() time.Time) + // userDescHasRestrictedAccess is the schema descriptor for has_restricted_access field. + userDescHasRestrictedAccess := userFields[3].Descriptor() + // user.DefaultHasRestrictedAccess holds the default value on creation for the has_restricted_access field. + user.DefaultHasRestrictedAccess = userDescHasRestrictedAccess.Default.(bool) // userDescID is the schema descriptor for id field. userDescID := userFields[1].Descriptor() // user.DefaultID holds the default value on creation for the id field. diff --git a/app/controlplane/pkg/data/ent/schema-viz.html b/app/controlplane/pkg/data/ent/schema-viz.html index 332a44126..d0f076be2 100644 --- a/app/controlplane/pkg/data/ent/schema-viz.html +++ b/app/controlplane/pkg/data/ent/schema-viz.html @@ -70,7 +70,7 @@ } - const entGraph = JSON.parse("{\"nodes\":[{\"id\":\"APIToken\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"expires_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Attestation\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"bundle\",\"type\":\"[]byte\"},{\"name\":\"workflowrun_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"CASBackend\",\"fields\":[{\"name\":\"location\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"provider\",\"type\":\"biz.CASBackendProvider\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"validation_status\",\"type\":\"biz.CASBackendValidationStatus\"},{\"name\":\"validated_at\",\"type\":\"time.Time\"},{\"name\":\"default\",\"type\":\"bool\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"fallback\",\"type\":\"bool\"},{\"name\":\"max_blob_size_bytes\",\"type\":\"int64\"}]},{\"id\":\"CASMapping\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_run_id\",\"type\":\"uuid.UUID\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Integration\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"IntegrationAttachment\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Membership\",\"fields\":[{\"name\":\"current\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"role\",\"type\":\"authz.Role\"}]},{\"id\":\"OrgInvitation\",\"fields\":[{\"name\":\"receiver_email\",\"type\":\"string\"},{\"name\":\"status\",\"type\":\"biz.OrgInvitationStatus\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"sender_id\",\"type\":\"uuid.UUID\"},{\"name\":\"role\",\"type\":\"authz.Role\"}]},{\"id\":\"Organization\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"block_on_policy_violation\",\"type\":\"bool\"}]},{\"id\":\"Project\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"ProjectVersion\",\"fields\":[{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"prerelease\",\"type\":\"bool\"},{\"name\":\"workflow_run_count\",\"type\":\"int\"},{\"name\":\"released_at\",\"type\":\"time.Time\"}]},{\"id\":\"Referrer\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"downloadable\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"metadata\",\"type\":\"map[string]string\"},{\"name\":\"annotations\",\"type\":\"map[string]string\"}]},{\"id\":\"RobotAccount\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"}]},{\"id\":\"User\",\"fields\":[{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"Workflow\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"project_old\",\"type\":\"string\"},{\"name\":\"team\",\"type\":\"string\"},{\"name\":\"runs_count\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"latest_run\",\"type\":\"uuid.UUID\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"metadata\",\"type\":\"map[string]interface {}\"}]},{\"id\":\"WorkflowContract\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"description\",\"type\":\"string\"}]},{\"id\":\"WorkflowContractVersion\",\"fields\":[{\"name\":\"body\",\"type\":\"[]byte\"},{\"name\":\"raw_body\",\"type\":\"[]byte\"},{\"name\":\"raw_body_format\",\"type\":\"unmarshal.RawFormat\"},{\"name\":\"revision\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowRun\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"finished_at\",\"type\":\"time.Time\"},{\"name\":\"state\",\"type\":\"biz.WorkflowRunStatus\"},{\"name\":\"reason\",\"type\":\"string\"},{\"name\":\"run_url\",\"type\":\"string\"},{\"name\":\"runner_type\",\"type\":\"string\"},{\"name\":\"attestation\",\"type\":\"*dsse.Envelope\"},{\"name\":\"attestation_digest\",\"type\":\"string\"},{\"name\":\"attestation_state\",\"type\":\"[]byte\"},{\"name\":\"contract_revision_used\",\"type\":\"int\"},{\"name\":\"contract_revision_latest\",\"type\":\"int\"},{\"name\":\"version_id\",\"type\":\"uuid.UUID\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]}],\"edges\":[{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"OrgInvitation\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"OrgInvitation\",\"to\":\"User\",\"label\":\"sender\"},{\"from\":\"Organization\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Organization\",\"to\":\"WorkflowContract\",\"label\":\"workflow_contracts\"},{\"from\":\"Organization\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Organization\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"Organization\",\"to\":\"Integration\",\"label\":\"integrations\"},{\"from\":\"Organization\",\"to\":\"APIToken\",\"label\":\"api_tokens\"},{\"from\":\"Organization\",\"to\":\"Project\",\"label\":\"projects\"},{\"from\":\"Project\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Project\",\"to\":\"ProjectVersion\",\"label\":\"versions\"},{\"from\":\"ProjectVersion\",\"to\":\"WorkflowRun\",\"label\":\"runs\"},{\"from\":\"Referrer\",\"to\":\"Referrer\",\"label\":\"references\"},{\"from\":\"Referrer\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"User\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Workflow\",\"to\":\"RobotAccount\",\"label\":\"robotaccounts\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"Workflow\",\"to\":\"WorkflowContract\",\"label\":\"contract\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"latest_workflow_run\"},{\"from\":\"WorkflowContract\",\"to\":\"WorkflowContractVersion\",\"label\":\"versions\"},{\"from\":\"WorkflowRun\",\"to\":\"WorkflowContractVersion\",\"label\":\"contract_version\"},{\"from\":\"WorkflowRun\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"WorkflowRun\",\"to\":\"Attestation\",\"label\":\"attestation_bundle\"}]}"); + const entGraph = JSON.parse("{\"nodes\":[{\"id\":\"APIToken\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"expires_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Attestation\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"bundle\",\"type\":\"[]byte\"},{\"name\":\"workflowrun_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"CASBackend\",\"fields\":[{\"name\":\"location\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"provider\",\"type\":\"biz.CASBackendProvider\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"validation_status\",\"type\":\"biz.CASBackendValidationStatus\"},{\"name\":\"validated_at\",\"type\":\"time.Time\"},{\"name\":\"default\",\"type\":\"bool\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"fallback\",\"type\":\"bool\"},{\"name\":\"max_blob_size_bytes\",\"type\":\"int64\"}]},{\"id\":\"CASMapping\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_run_id\",\"type\":\"uuid.UUID\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Integration\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"IntegrationAttachment\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Membership\",\"fields\":[{\"name\":\"current\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"role\",\"type\":\"authz.Role\"}]},{\"id\":\"OrgInvitation\",\"fields\":[{\"name\":\"receiver_email\",\"type\":\"string\"},{\"name\":\"status\",\"type\":\"biz.OrgInvitationStatus\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"sender_id\",\"type\":\"uuid.UUID\"},{\"name\":\"role\",\"type\":\"authz.Role\"}]},{\"id\":\"Organization\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"block_on_policy_violation\",\"type\":\"bool\"}]},{\"id\":\"Project\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"ProjectVersion\",\"fields\":[{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"prerelease\",\"type\":\"bool\"},{\"name\":\"workflow_run_count\",\"type\":\"int\"},{\"name\":\"released_at\",\"type\":\"time.Time\"}]},{\"id\":\"Referrer\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"downloadable\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"metadata\",\"type\":\"map[string]string\"},{\"name\":\"annotations\",\"type\":\"map[string]string\"}]},{\"id\":\"RobotAccount\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"}]},{\"id\":\"User\",\"fields\":[{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"has_restricted_access\",\"type\":\"bool\"}]},{\"id\":\"Workflow\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"project_old\",\"type\":\"string\"},{\"name\":\"team\",\"type\":\"string\"},{\"name\":\"runs_count\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"latest_run\",\"type\":\"uuid.UUID\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"metadata\",\"type\":\"map[string]interface {}\"}]},{\"id\":\"WorkflowContract\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"description\",\"type\":\"string\"}]},{\"id\":\"WorkflowContractVersion\",\"fields\":[{\"name\":\"body\",\"type\":\"[]byte\"},{\"name\":\"raw_body\",\"type\":\"[]byte\"},{\"name\":\"raw_body_format\",\"type\":\"unmarshal.RawFormat\"},{\"name\":\"revision\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowRun\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"finished_at\",\"type\":\"time.Time\"},{\"name\":\"state\",\"type\":\"biz.WorkflowRunStatus\"},{\"name\":\"reason\",\"type\":\"string\"},{\"name\":\"run_url\",\"type\":\"string\"},{\"name\":\"runner_type\",\"type\":\"string\"},{\"name\":\"attestation\",\"type\":\"*dsse.Envelope\"},{\"name\":\"attestation_digest\",\"type\":\"string\"},{\"name\":\"attestation_state\",\"type\":\"[]byte\"},{\"name\":\"contract_revision_used\",\"type\":\"int\"},{\"name\":\"contract_revision_latest\",\"type\":\"int\"},{\"name\":\"version_id\",\"type\":\"uuid.UUID\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]}],\"edges\":[{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"OrgInvitation\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"OrgInvitation\",\"to\":\"User\",\"label\":\"sender\"},{\"from\":\"Organization\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Organization\",\"to\":\"WorkflowContract\",\"label\":\"workflow_contracts\"},{\"from\":\"Organization\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Organization\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"Organization\",\"to\":\"Integration\",\"label\":\"integrations\"},{\"from\":\"Organization\",\"to\":\"APIToken\",\"label\":\"api_tokens\"},{\"from\":\"Organization\",\"to\":\"Project\",\"label\":\"projects\"},{\"from\":\"Project\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Project\",\"to\":\"ProjectVersion\",\"label\":\"versions\"},{\"from\":\"ProjectVersion\",\"to\":\"WorkflowRun\",\"label\":\"runs\"},{\"from\":\"Referrer\",\"to\":\"Referrer\",\"label\":\"references\"},{\"from\":\"Referrer\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"User\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Workflow\",\"to\":\"RobotAccount\",\"label\":\"robotaccounts\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"Workflow\",\"to\":\"WorkflowContract\",\"label\":\"contract\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"latest_workflow_run\"},{\"from\":\"WorkflowContract\",\"to\":\"WorkflowContractVersion\",\"label\":\"versions\"},{\"from\":\"WorkflowRun\",\"to\":\"WorkflowContractVersion\",\"label\":\"contract_version\"},{\"from\":\"WorkflowRun\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"WorkflowRun\",\"to\":\"Attestation\",\"label\":\"attestation_bundle\"}]}"); const nodes = new vis.DataSet((entGraph.nodes || []).map(n => ({ id: n.id, diff --git a/app/controlplane/pkg/data/ent/schema/user.go b/app/controlplane/pkg/data/ent/schema/user.go index 6a575c6a2..fd9e44488 100644 --- a/app/controlplane/pkg/data/ent/schema/user.go +++ b/app/controlplane/pkg/data/ent/schema/user.go @@ -19,6 +19,8 @@ import ( "net/mail" "time" + "entgo.io/ent/schema/index" + "entgo.io/ent" "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema/edge" @@ -47,6 +49,7 @@ func (User) Fields() []ent.Field { Annotations(&entsql.Annotation{ Default: "CURRENT_TIMESTAMP", }), + field.Bool("has_restricted_access").Default(true).Comment("Whether the user is blocked from accessing the system"), } } @@ -56,3 +59,11 @@ func (User) Edges() []ent.Edge { edge.To("memberships", Membership.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), } } + +func (User) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("has_restricted_access").Annotations( + entsql.IndexWhere("has_restricted_access IS true"), + ), + } +} diff --git a/app/controlplane/pkg/data/ent/user.go b/app/controlplane/pkg/data/ent/user.go index d87a4b428..623589943 100644 --- a/app/controlplane/pkg/data/ent/user.go +++ b/app/controlplane/pkg/data/ent/user.go @@ -22,6 +22,8 @@ type User struct { Email string `json:"email,omitempty"` // CreatedAt holds the value of the "created_at" field. CreatedAt time.Time `json:"created_at,omitempty"` + // Whether the user is blocked from accessing the system + HasRestrictedAccess bool `json:"has_restricted_access,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the UserQuery when eager-loading is set. Edges UserEdges `json:"edges"` @@ -51,6 +53,8 @@ func (*User) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { + case user.FieldHasRestrictedAccess: + values[i] = new(sql.NullBool) case user.FieldEmail: values[i] = new(sql.NullString) case user.FieldCreatedAt: @@ -90,6 +94,12 @@ func (u *User) assignValues(columns []string, values []any) error { } else if value.Valid { u.CreatedAt = value.Time } + case user.FieldHasRestrictedAccess: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field has_restricted_access", values[i]) + } else if value.Valid { + u.HasRestrictedAccess = value.Bool + } default: u.selectValues.Set(columns[i], values[i]) } @@ -136,6 +146,9 @@ func (u *User) String() string { builder.WriteString(", ") builder.WriteString("created_at=") builder.WriteString(u.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("has_restricted_access=") + builder.WriteString(fmt.Sprintf("%v", u.HasRestrictedAccess)) builder.WriteByte(')') return builder.String() } diff --git a/app/controlplane/pkg/data/ent/user/user.go b/app/controlplane/pkg/data/ent/user/user.go index 351826285..7fbc9b7ff 100644 --- a/app/controlplane/pkg/data/ent/user/user.go +++ b/app/controlplane/pkg/data/ent/user/user.go @@ -19,6 +19,8 @@ const ( FieldEmail = "email" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" + // FieldHasRestrictedAccess holds the string denoting the has_restricted_access field in the database. + FieldHasRestrictedAccess = "has_restricted_access" // EdgeMemberships holds the string denoting the memberships edge name in mutations. EdgeMemberships = "memberships" // Table holds the table name of the user in the database. @@ -37,6 +39,7 @@ var Columns = []string{ FieldID, FieldEmail, FieldCreatedAt, + FieldHasRestrictedAccess, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -54,6 +57,8 @@ var ( EmailValidator func(string) error // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() time.Time + // DefaultHasRestrictedAccess holds the default value on creation for the "has_restricted_access" field. + DefaultHasRestrictedAccess bool // DefaultID holds the default value on creation for the "id" field. DefaultID func() uuid.UUID ) @@ -76,6 +81,11 @@ func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() } +// ByHasRestrictedAccess orders the results by the has_restricted_access field. +func ByHasRestrictedAccess(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldHasRestrictedAccess, opts...).ToFunc() +} + // ByMembershipsCount orders the results by memberships count. func ByMembershipsCount(opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/user/where.go b/app/controlplane/pkg/data/ent/user/where.go index 1f531f565..1f168ba06 100644 --- a/app/controlplane/pkg/data/ent/user/where.go +++ b/app/controlplane/pkg/data/ent/user/where.go @@ -66,6 +66,11 @@ func CreatedAt(v time.Time) predicate.User { return predicate.User(sql.FieldEQ(FieldCreatedAt, v)) } +// HasRestrictedAccess applies equality check predicate on the "has_restricted_access" field. It's identical to HasRestrictedAccessEQ. +func HasRestrictedAccess(v bool) predicate.User { + return predicate.User(sql.FieldEQ(FieldHasRestrictedAccess, v)) +} + // EmailEQ applies the EQ predicate on the "email" field. func EmailEQ(v string) predicate.User { return predicate.User(sql.FieldEQ(FieldEmail, v)) @@ -171,6 +176,16 @@ func CreatedAtLTE(v time.Time) predicate.User { return predicate.User(sql.FieldLTE(FieldCreatedAt, v)) } +// HasRestrictedAccessEQ applies the EQ predicate on the "has_restricted_access" field. +func HasRestrictedAccessEQ(v bool) predicate.User { + return predicate.User(sql.FieldEQ(FieldHasRestrictedAccess, v)) +} + +// HasRestrictedAccessNEQ applies the NEQ predicate on the "has_restricted_access" field. +func HasRestrictedAccessNEQ(v bool) predicate.User { + return predicate.User(sql.FieldNEQ(FieldHasRestrictedAccess, v)) +} + // HasMemberships applies the HasEdge predicate on the "memberships" edge. func HasMemberships() predicate.User { return predicate.User(func(s *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/user_create.go b/app/controlplane/pkg/data/ent/user_create.go index 29c562b45..bea67b243 100644 --- a/app/controlplane/pkg/data/ent/user_create.go +++ b/app/controlplane/pkg/data/ent/user_create.go @@ -45,6 +45,20 @@ func (uc *UserCreate) SetNillableCreatedAt(t *time.Time) *UserCreate { return uc } +// SetHasRestrictedAccess sets the "has_restricted_access" field. +func (uc *UserCreate) SetHasRestrictedAccess(b bool) *UserCreate { + uc.mutation.SetHasRestrictedAccess(b) + return uc +} + +// SetNillableHasRestrictedAccess sets the "has_restricted_access" field if the given value is not nil. +func (uc *UserCreate) SetNillableHasRestrictedAccess(b *bool) *UserCreate { + if b != nil { + uc.SetHasRestrictedAccess(*b) + } + return uc +} + // SetID sets the "id" field. func (uc *UserCreate) SetID(u uuid.UUID) *UserCreate { uc.mutation.SetID(u) @@ -113,6 +127,10 @@ func (uc *UserCreate) defaults() { v := user.DefaultCreatedAt() uc.mutation.SetCreatedAt(v) } + if _, ok := uc.mutation.HasRestrictedAccess(); !ok { + v := user.DefaultHasRestrictedAccess + uc.mutation.SetHasRestrictedAccess(v) + } if _, ok := uc.mutation.ID(); !ok { v := user.DefaultID() uc.mutation.SetID(v) @@ -132,6 +150,9 @@ func (uc *UserCreate) check() error { if _, ok := uc.mutation.CreatedAt(); !ok { return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "User.created_at"`)} } + if _, ok := uc.mutation.HasRestrictedAccess(); !ok { + return &ValidationError{Name: "has_restricted_access", err: errors.New(`ent: missing required field "User.has_restricted_access"`)} + } return nil } @@ -176,6 +197,10 @@ func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { _spec.SetField(user.FieldCreatedAt, field.TypeTime, value) _node.CreatedAt = value } + if value, ok := uc.mutation.HasRestrictedAccess(); ok { + _spec.SetField(user.FieldHasRestrictedAccess, field.TypeBool, value) + _node.HasRestrictedAccess = value + } if nodes := uc.mutation.MembershipsIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -256,6 +281,18 @@ func (u *UserUpsert) UpdateEmail() *UserUpsert { return u } +// SetHasRestrictedAccess sets the "has_restricted_access" field. +func (u *UserUpsert) SetHasRestrictedAccess(v bool) *UserUpsert { + u.Set(user.FieldHasRestrictedAccess, v) + return u +} + +// UpdateHasRestrictedAccess sets the "has_restricted_access" field to the value that was provided on create. +func (u *UserUpsert) UpdateHasRestrictedAccess() *UserUpsert { + u.SetExcluded(user.FieldHasRestrictedAccess) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. // Using this option is equivalent to using: // @@ -321,6 +358,20 @@ func (u *UserUpsertOne) UpdateEmail() *UserUpsertOne { }) } +// SetHasRestrictedAccess sets the "has_restricted_access" field. +func (u *UserUpsertOne) SetHasRestrictedAccess(v bool) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetHasRestrictedAccess(v) + }) +} + +// UpdateHasRestrictedAccess sets the "has_restricted_access" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateHasRestrictedAccess() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateHasRestrictedAccess() + }) +} + // Exec executes the query. func (u *UserUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -553,6 +604,20 @@ func (u *UserUpsertBulk) UpdateEmail() *UserUpsertBulk { }) } +// SetHasRestrictedAccess sets the "has_restricted_access" field. +func (u *UserUpsertBulk) SetHasRestrictedAccess(v bool) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetHasRestrictedAccess(v) + }) +} + +// UpdateHasRestrictedAccess sets the "has_restricted_access" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateHasRestrictedAccess() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateHasRestrictedAccess() + }) +} + // Exec executes the query. func (u *UserUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/app/controlplane/pkg/data/ent/user_update.go b/app/controlplane/pkg/data/ent/user_update.go index f7794bbae..a05795fc7 100644 --- a/app/controlplane/pkg/data/ent/user_update.go +++ b/app/controlplane/pkg/data/ent/user_update.go @@ -44,6 +44,20 @@ func (uu *UserUpdate) SetNillableEmail(s *string) *UserUpdate { return uu } +// SetHasRestrictedAccess sets the "has_restricted_access" field. +func (uu *UserUpdate) SetHasRestrictedAccess(b bool) *UserUpdate { + uu.mutation.SetHasRestrictedAccess(b) + return uu +} + +// SetNillableHasRestrictedAccess sets the "has_restricted_access" field if the given value is not nil. +func (uu *UserUpdate) SetNillableHasRestrictedAccess(b *bool) *UserUpdate { + if b != nil { + uu.SetHasRestrictedAccess(*b) + } + return uu +} + // AddMembershipIDs adds the "memberships" edge to the Membership entity by IDs. func (uu *UserUpdate) AddMembershipIDs(ids ...uuid.UUID) *UserUpdate { uu.mutation.AddMembershipIDs(ids...) @@ -143,6 +157,9 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := uu.mutation.Email(); ok { _spec.SetField(user.FieldEmail, field.TypeString, value) } + if value, ok := uu.mutation.HasRestrictedAccess(); ok { + _spec.SetField(user.FieldHasRestrictedAccess, field.TypeBool, value) + } if uu.mutation.MembershipsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -224,6 +241,20 @@ func (uuo *UserUpdateOne) SetNillableEmail(s *string) *UserUpdateOne { return uuo } +// SetHasRestrictedAccess sets the "has_restricted_access" field. +func (uuo *UserUpdateOne) SetHasRestrictedAccess(b bool) *UserUpdateOne { + uuo.mutation.SetHasRestrictedAccess(b) + return uuo +} + +// SetNillableHasRestrictedAccess sets the "has_restricted_access" field if the given value is not nil. +func (uuo *UserUpdateOne) SetNillableHasRestrictedAccess(b *bool) *UserUpdateOne { + if b != nil { + uuo.SetHasRestrictedAccess(*b) + } + return uuo +} + // AddMembershipIDs adds the "memberships" edge to the Membership entity by IDs. func (uuo *UserUpdateOne) AddMembershipIDs(ids ...uuid.UUID) *UserUpdateOne { uuo.mutation.AddMembershipIDs(ids...) @@ -353,6 +384,9 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) if value, ok := uuo.mutation.Email(); ok { _spec.SetField(user.FieldEmail, field.TypeString, value) } + if value, ok := uuo.mutation.HasRestrictedAccess(); ok { + _spec.SetField(user.FieldHasRestrictedAccess, field.TypeBool, value) + } if uuo.mutation.MembershipsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/app/controlplane/pkg/data/user.go b/app/controlplane/pkg/data/user.go index 57de5ade9..445f78272 100644 --- a/app/controlplane/pkg/data/user.go +++ b/app/controlplane/pkg/data/user.go @@ -17,10 +17,14 @@ package data import ( "context" + "fmt" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/user" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" + "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" ) @@ -76,6 +80,66 @@ func (r *userRepo) Delete(ctx context.Context, userID uuid.UUID) (err error) { return r.data.DB.User.DeleteOneID(userID).Exec(ctx) } +// UpdateAccess updates the access restriction for a user +func (r *userRepo) UpdateAccess(ctx context.Context, userID uuid.UUID, isAccessRestricted bool) error { + _, err := r.data.DB.User.UpdateOneID(userID).SetHasRestrictedAccess(isAccessRestricted).Save(ctx) + if err != nil { + return fmt.Errorf("error updating user access: %w", err) + } + + return nil +} + +// FindAll get all users in the system using pagination +func (r *userRepo) FindAll(ctx context.Context, pagination *pagination.OffsetPaginationOpts) ([]*biz.User, int, error) { + if pagination == nil { + return nil, 0, fmt.Errorf("pagination options is required") + } + + baseQuery := r.data.DB.User.Query() + + count, err := baseQuery.Count(ctx) + if err != nil { + return nil, 0, err + } + + users, err := baseQuery. + Order(ent.Desc(workflow.FieldCreatedAt)). + Limit(pagination.Limit()). + Offset(pagination.Offset()). + All(ctx) + if err != nil { + return nil, 0, err + } + + result := make([]*biz.User, 0, len(users)) + for _, u := range users { + result = append(result, entUserToBizUser(u)) + } + + return result, count, nil +} + +// CountUsersWithRestrictedAccess returns the number of users with restricted access +func (r *userRepo) CountUsersWithRestrictedAccess(ctx context.Context) (int, error) { + return r.data.DB.User.Query(). + Where(user.HasRestrictedAccess(true)). + Count(ctx) +} + +// UpdateAllUsersAccess updates the access restriction for all users +func (r *userRepo) UpdateAllUsersAccess(ctx context.Context, isAccessRestricted bool) error { + _, err := r.data.DB.User.Update(). + Where(user.HasRestrictedAccess(!isAccessRestricted)). + SetHasRestrictedAccess(isAccessRestricted). + Save(ctx) + if err != nil { + return fmt.Errorf("error updating all users access: %w", err) + } + + return nil +} + func entUserToBizUser(eu *ent.User) *biz.User { return &biz.User{Email: eu.Email, ID: eu.ID.String(), CreatedAt: toTimePtr(eu.CreatedAt)} } From 5ac207271b2dd2b935cd4a1ec7f2972c020f22c5 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 27 Mar 2025 11:44:39 +0100 Subject: [PATCH 2/4] export has restricted access field Signed-off-by: Javier Rodriguez --- app/controlplane/pkg/biz/user.go | 7 ++++--- app/controlplane/pkg/data/user.go | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/controlplane/pkg/biz/user.go b/app/controlplane/pkg/biz/user.go index e224a58d8..28f89c2b5 100644 --- a/app/controlplane/pkg/biz/user.go +++ b/app/controlplane/pkg/biz/user.go @@ -33,9 +33,10 @@ import ( ) type User struct { - ID string - Email string - CreatedAt *time.Time + ID string + Email string + CreatedAt *time.Time + HasRestrictedAccess bool } type UserRepo interface { diff --git a/app/controlplane/pkg/data/user.go b/app/controlplane/pkg/data/user.go index 445f78272..68aebc3b9 100644 --- a/app/controlplane/pkg/data/user.go +++ b/app/controlplane/pkg/data/user.go @@ -141,5 +141,10 @@ func (r *userRepo) UpdateAllUsersAccess(ctx context.Context, isAccessRestricted } func entUserToBizUser(eu *ent.User) *biz.User { - return &biz.User{Email: eu.Email, ID: eu.ID.String(), CreatedAt: toTimePtr(eu.CreatedAt)} + return &biz.User{ + Email: eu.Email, + ID: eu.ID.String(), + CreatedAt: toTimePtr(eu.CreatedAt), + HasRestrictedAccess: eu.HasRestrictedAccess, + } } From 5f4b4d52550669410c981c7bbfccd8a7c878d7ba Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 27 Mar 2025 13:23:57 +0100 Subject: [PATCH 3/4] tackle feedback Signed-off-by: Javier Rodriguez --- app/controlplane/pkg/biz/user.go | 1 - app/controlplane/pkg/biz/useraccess_syncer.go | 58 ++++++++++--------- ...{20250327094547.sql => 20250327120254.sql} | 4 +- .../pkg/data/ent/migrate/migrations/atlas.sum | 4 +- .../pkg/data/ent/migrate/schema.go | 5 +- app/controlplane/pkg/data/ent/mutation.go | 24 +++++++- app/controlplane/pkg/data/ent/runtime.go | 4 -- app/controlplane/pkg/data/ent/schema/user.go | 6 +- app/controlplane/pkg/data/ent/user.go | 2 +- app/controlplane/pkg/data/ent/user/user.go | 2 - app/controlplane/pkg/data/ent/user/where.go | 10 ++++ app/controlplane/pkg/data/ent/user_create.go | 27 ++++++--- app/controlplane/pkg/data/ent/user_update.go | 18 ++++++ app/controlplane/pkg/data/user.go | 13 ----- 14 files changed, 110 insertions(+), 68 deletions(-) rename app/controlplane/pkg/data/ent/migrate/migrations/{20250327094547.sql => 20250327120254.sql} (73%) diff --git a/app/controlplane/pkg/biz/user.go b/app/controlplane/pkg/biz/user.go index 28f89c2b5..65bc7b3da 100644 --- a/app/controlplane/pkg/biz/user.go +++ b/app/controlplane/pkg/biz/user.go @@ -47,7 +47,6 @@ type UserRepo interface { FindAll(ctx context.Context, pagination *pagination.OffsetPaginationOpts) ([]*User, int, error) UpdateAccess(ctx context.Context, userID uuid.UUID, isAccessRestricted bool) error CountUsersWithRestrictedAccess(ctx context.Context) (int, error) - UpdateAllUsersAccess(ctx context.Context, isAccessRestricted bool) error } type UserOrgFinder interface { diff --git a/app/controlplane/pkg/biz/useraccess_syncer.go b/app/controlplane/pkg/biz/useraccess_syncer.go index 40d1141df..fe5735b24 100644 --- a/app/controlplane/pkg/biz/useraccess_syncer.go +++ b/app/controlplane/pkg/biz/useraccess_syncer.go @@ -57,14 +57,8 @@ func (u *UserAccessSyncerUseCase) StartSyncingUserAccess(ctx context.Context) er case <-ticker.C: u.logger.Infow("msg", "Syncing user access") - // Count the number of users with restricted access - usersWithRestrictedAccess, err := u.userRepo.CountUsersWithRestrictedAccess(ctx) - if err != nil { - return fmt.Errorf("count users with restricted access: %w", err) - } - // Update the access restriction status of all users based on the allowlist - if err := u.updateUserAccessBasedOnAllowList(ctx, usersWithRestrictedAccess); err != nil { + if err := u.updateUserAccessBasedOnAllowList(ctx); err != nil { return fmt.Errorf("update user access based on allow list: %w", err) } @@ -74,19 +68,25 @@ func (u *UserAccessSyncerUseCase) StartSyncingUserAccess(ctx context.Context) er } // updateUserAccessBasedOnAllowList updates the access restriction status of all users based on the allowlist -func (u *UserAccessSyncerUseCase) updateUserAccessBasedOnAllowList(ctx context.Context, usersWithRestrictedAccess int) error { - // If the allowlist is empty and there are users with restricted access, give access to those users - if u.allowList != nil && len(u.allowList.GetRules()) == 0 && usersWithRestrictedAccess > 0 { - if err := u.userRepo.UpdateAllUsersAccess(ctx, false); err != nil { - return fmt.Errorf("update all users access: %w", err) - } - } else { - // Sync the access restriction status of all users based on the allowlist - if err := u.syncUserAccess(ctx); err != nil { - return fmt.Errorf("sync user access: %w", err) +func (u *UserAccessSyncerUseCase) updateUserAccessBasedOnAllowList(ctx context.Context) error { + // Count the number of users with restricted access + usersWithRestrictedAccess, err := u.userRepo.CountUsersWithRestrictedAccess(ctx) + if err != nil { + return fmt.Errorf("count users with access: %w", err) + } + + // If the allowlist is empty and there are no users with restricted access, we can skip the sync + if u.allowList == nil || len(u.allowList.GetRules()) == 0 { + if usersWithRestrictedAccess == 0 { + return nil } } + // Sync the access restriction status of all users based on the allowlist + if err := u.syncUserAccess(ctx); err != nil { + return fmt.Errorf("sync user access: %w", err) + } + return nil } @@ -108,11 +108,8 @@ func (u *UserAccessSyncerUseCase) syncUserAccess(ctx context.Context) error { return fmt.Errorf("failed to list users: %w", err) } - // If the allowlist is empty, we deactivate the access restriction for all users - isAllowListDeactivated := u.allowList == nil || len(u.allowList.GetRules()) == 0 - for _, user := range users { - if err := u.updateUserAccessRestriction(ctx, user, isAllowListDeactivated); err != nil { + if err := u.updateUserAccessRestriction(ctx, user); err != nil { return fmt.Errorf("failed to update user access: %w", err) } } @@ -128,15 +125,22 @@ func (u *UserAccessSyncerUseCase) syncUserAccess(ctx context.Context) error { } // updateUserAccessRestriction updates the access restriction status of a user -func (u *UserAccessSyncerUseCase) updateUserAccessRestriction(ctx context.Context, user *User, isAllowListDeactivated bool) error { - allow, err := UserEmailInAllowlist(u.allowList.GetRules(), user.Email) - if err != nil { - return fmt.Errorf("error checking user in allowList: %w", err) - } +func (u *UserAccessSyncerUseCase) updateUserAccessRestriction(ctx context.Context, user *User) error { + isAllowListDeactivated := u.allowList == nil || len(u.allowList.GetRules()) == 0 + + var isAccessRestricted bool - isAccessRestricted := !allow + // If the allowlist is empty, we deactivate the access restriction for all users if isAllowListDeactivated { isAccessRestricted = false + } else { + // Check if the user email is in the allowlist and update the access restriction status accordingly + allow, err := UserEmailInAllowlist(u.allowList.GetRules(), user.Email) + if err != nil { + return fmt.Errorf("error checking user in allowList: %w", err) + } + + isAccessRestricted = !allow } parsedUserUUID, err := uuid.Parse(user.ID) diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250327094547.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250327120254.sql similarity index 73% rename from app/controlplane/pkg/data/ent/migrate/migrations/20250327094547.sql rename to app/controlplane/pkg/data/ent/migrate/migrations/20250327120254.sql index c722a95e5..2278d8829 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/20250327094547.sql +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250327120254.sql @@ -1,4 +1,4 @@ -- Modify "users" table -ALTER TABLE "users" ADD COLUMN "has_restricted_access" boolean NOT NULL DEFAULT true; +ALTER TABLE "users" ADD COLUMN "has_restricted_access" boolean NULL; -- Create index "user_has_restricted_access" to table: "users" -CREATE INDEX "user_has_restricted_access" ON "users" ("has_restricted_access") WHERE (has_restricted_access IS TRUE); +CREATE INDEX "user_has_restricted_access" ON "users" ("has_restricted_access"); diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 8d83aa971..e62167436 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:FURGzv6iR93WTD2fkOT2M1QnDf5bRjulR0oU5ygDzh8= +h1:6sY+6+wQ/2pBGPP/+t/PcZkL1HxZR2wcel340cIBLAo= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -79,4 +79,4 @@ h1:FURGzv6iR93WTD2fkOT2M1QnDf5bRjulR0oU5ygDzh8= 20250203084822.sql h1:xKQ2szI/uaQjz9mOthOE5SFO8wV/maJglRfciXjb+P8= 20250303153626.sql h1:y38iNqTO+lutsb2hPu+gepPDgSDmsTcWbgu7kMpbIzE= 20250326110627.sql h1:kTneMHSqpE7I8Gl88jjTy2olXpdg/np0yA45lqIxBic= -20250327094547.sql h1:qSl1MtpeZWr2EnDk83X7VmglDVF/Oyp8FkRhDHB1Lsw= +20250327120254.sql h1:g7J945QzvonLcydhUryeIt2qXX/BLRo8XdClt2B6264= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index eb3bc7ecd..27f0754d4 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -423,7 +423,7 @@ var ( {Name: "id", Type: field.TypeUUID, Unique: true}, {Name: "email", Type: field.TypeString, Unique: true}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, - {Name: "has_restricted_access", Type: field.TypeBool, Default: true}, + {Name: "has_restricted_access", Type: field.TypeBool, Nullable: true}, } // UsersTable holds the schema information for the "users" table. UsersTable = &schema.Table{ @@ -435,9 +435,6 @@ var ( Name: "user_has_restricted_access", Unique: false, Columns: []*schema.Column{UsersColumns[3]}, - Annotation: &entsql.IndexAnnotation{ - Where: "has_restricted_access IS true", - }, }, }, } diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index c80efca92..056d1b6cb 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -10363,9 +10363,22 @@ func (m *UserMutation) OldHasRestrictedAccess(ctx context.Context) (v bool, err return oldValue.HasRestrictedAccess, nil } +// ClearHasRestrictedAccess clears the value of the "has_restricted_access" field. +func (m *UserMutation) ClearHasRestrictedAccess() { + m.has_restricted_access = nil + m.clearedFields[user.FieldHasRestrictedAccess] = struct{}{} +} + +// HasRestrictedAccessCleared returns if the "has_restricted_access" field was cleared in this mutation. +func (m *UserMutation) HasRestrictedAccessCleared() bool { + _, ok := m.clearedFields[user.FieldHasRestrictedAccess] + return ok +} + // ResetHasRestrictedAccess resets all changes to the "has_restricted_access" field. func (m *UserMutation) ResetHasRestrictedAccess() { m.has_restricted_access = nil + delete(m.clearedFields, user.FieldHasRestrictedAccess) } // AddMembershipIDs adds the "memberships" edge to the Membership entity by ids. @@ -10554,7 +10567,11 @@ func (m *UserMutation) AddField(name string, value ent.Value) error { // ClearedFields returns all nullable fields that were cleared during this // mutation. func (m *UserMutation) ClearedFields() []string { - return nil + var fields []string + if m.FieldCleared(user.FieldHasRestrictedAccess) { + fields = append(fields, user.FieldHasRestrictedAccess) + } + return fields } // FieldCleared returns a boolean indicating if a field with the given name was @@ -10567,6 +10584,11 @@ func (m *UserMutation) FieldCleared(name string) bool { // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. func (m *UserMutation) ClearField(name string) error { + switch name { + case user.FieldHasRestrictedAccess: + m.ClearHasRestrictedAccess() + return nil + } return fmt.Errorf("unknown User nullable field %s", name) } diff --git a/app/controlplane/pkg/data/ent/runtime.go b/app/controlplane/pkg/data/ent/runtime.go index a90ce459e..3a3b6b834 100644 --- a/app/controlplane/pkg/data/ent/runtime.go +++ b/app/controlplane/pkg/data/ent/runtime.go @@ -217,10 +217,6 @@ func init() { userDescCreatedAt := userFields[2].Descriptor() // user.DefaultCreatedAt holds the default value on creation for the created_at field. user.DefaultCreatedAt = userDescCreatedAt.Default.(func() time.Time) - // userDescHasRestrictedAccess is the schema descriptor for has_restricted_access field. - userDescHasRestrictedAccess := userFields[3].Descriptor() - // user.DefaultHasRestrictedAccess holds the default value on creation for the has_restricted_access field. - user.DefaultHasRestrictedAccess = userDescHasRestrictedAccess.Default.(bool) // userDescID is the schema descriptor for id field. userDescID := userFields[1].Descriptor() // user.DefaultID holds the default value on creation for the id field. diff --git a/app/controlplane/pkg/data/ent/schema/user.go b/app/controlplane/pkg/data/ent/schema/user.go index fd9e44488..ebd915709 100644 --- a/app/controlplane/pkg/data/ent/schema/user.go +++ b/app/controlplane/pkg/data/ent/schema/user.go @@ -49,7 +49,7 @@ func (User) Fields() []ent.Field { Annotations(&entsql.Annotation{ Default: "CURRENT_TIMESTAMP", }), - field.Bool("has_restricted_access").Default(true).Comment("Whether the user is blocked from accessing the system"), + field.Bool("has_restricted_access").Optional(), } } @@ -62,8 +62,6 @@ func (User) Edges() []ent.Edge { func (User) Indexes() []ent.Index { return []ent.Index{ - index.Fields("has_restricted_access").Annotations( - entsql.IndexWhere("has_restricted_access IS true"), - ), + index.Fields("has_restricted_access"), } } diff --git a/app/controlplane/pkg/data/ent/user.go b/app/controlplane/pkg/data/ent/user.go index 623589943..ff375608c 100644 --- a/app/controlplane/pkg/data/ent/user.go +++ b/app/controlplane/pkg/data/ent/user.go @@ -22,7 +22,7 @@ type User struct { Email string `json:"email,omitempty"` // CreatedAt holds the value of the "created_at" field. CreatedAt time.Time `json:"created_at,omitempty"` - // Whether the user is blocked from accessing the system + // HasRestrictedAccess holds the value of the "has_restricted_access" field. HasRestrictedAccess bool `json:"has_restricted_access,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the UserQuery when eager-loading is set. diff --git a/app/controlplane/pkg/data/ent/user/user.go b/app/controlplane/pkg/data/ent/user/user.go index 7fbc9b7ff..65038739b 100644 --- a/app/controlplane/pkg/data/ent/user/user.go +++ b/app/controlplane/pkg/data/ent/user/user.go @@ -57,8 +57,6 @@ var ( EmailValidator func(string) error // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() time.Time - // DefaultHasRestrictedAccess holds the default value on creation for the "has_restricted_access" field. - DefaultHasRestrictedAccess bool // DefaultID holds the default value on creation for the "id" field. DefaultID func() uuid.UUID ) diff --git a/app/controlplane/pkg/data/ent/user/where.go b/app/controlplane/pkg/data/ent/user/where.go index 1f168ba06..1c8746fee 100644 --- a/app/controlplane/pkg/data/ent/user/where.go +++ b/app/controlplane/pkg/data/ent/user/where.go @@ -186,6 +186,16 @@ func HasRestrictedAccessNEQ(v bool) predicate.User { return predicate.User(sql.FieldNEQ(FieldHasRestrictedAccess, v)) } +// HasRestrictedAccessIsNil applies the IsNil predicate on the "has_restricted_access" field. +func HasRestrictedAccessIsNil() predicate.User { + return predicate.User(sql.FieldIsNull(FieldHasRestrictedAccess)) +} + +// HasRestrictedAccessNotNil applies the NotNil predicate on the "has_restricted_access" field. +func HasRestrictedAccessNotNil() predicate.User { + return predicate.User(sql.FieldNotNull(FieldHasRestrictedAccess)) +} + // HasMemberships applies the HasEdge predicate on the "memberships" edge. func HasMemberships() predicate.User { return predicate.User(func(s *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/user_create.go b/app/controlplane/pkg/data/ent/user_create.go index bea67b243..fb29b51cc 100644 --- a/app/controlplane/pkg/data/ent/user_create.go +++ b/app/controlplane/pkg/data/ent/user_create.go @@ -127,10 +127,6 @@ func (uc *UserCreate) defaults() { v := user.DefaultCreatedAt() uc.mutation.SetCreatedAt(v) } - if _, ok := uc.mutation.HasRestrictedAccess(); !ok { - v := user.DefaultHasRestrictedAccess - uc.mutation.SetHasRestrictedAccess(v) - } if _, ok := uc.mutation.ID(); !ok { v := user.DefaultID() uc.mutation.SetID(v) @@ -150,9 +146,6 @@ func (uc *UserCreate) check() error { if _, ok := uc.mutation.CreatedAt(); !ok { return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "User.created_at"`)} } - if _, ok := uc.mutation.HasRestrictedAccess(); !ok { - return &ValidationError{Name: "has_restricted_access", err: errors.New(`ent: missing required field "User.has_restricted_access"`)} - } return nil } @@ -293,6 +286,12 @@ func (u *UserUpsert) UpdateHasRestrictedAccess() *UserUpsert { return u } +// ClearHasRestrictedAccess clears the value of the "has_restricted_access" field. +func (u *UserUpsert) ClearHasRestrictedAccess() *UserUpsert { + u.SetNull(user.FieldHasRestrictedAccess) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. // Using this option is equivalent to using: // @@ -372,6 +371,13 @@ func (u *UserUpsertOne) UpdateHasRestrictedAccess() *UserUpsertOne { }) } +// ClearHasRestrictedAccess clears the value of the "has_restricted_access" field. +func (u *UserUpsertOne) ClearHasRestrictedAccess() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.ClearHasRestrictedAccess() + }) +} + // Exec executes the query. func (u *UserUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -618,6 +624,13 @@ func (u *UserUpsertBulk) UpdateHasRestrictedAccess() *UserUpsertBulk { }) } +// ClearHasRestrictedAccess clears the value of the "has_restricted_access" field. +func (u *UserUpsertBulk) ClearHasRestrictedAccess() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.ClearHasRestrictedAccess() + }) +} + // Exec executes the query. func (u *UserUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/app/controlplane/pkg/data/ent/user_update.go b/app/controlplane/pkg/data/ent/user_update.go index a05795fc7..dcd508051 100644 --- a/app/controlplane/pkg/data/ent/user_update.go +++ b/app/controlplane/pkg/data/ent/user_update.go @@ -58,6 +58,12 @@ func (uu *UserUpdate) SetNillableHasRestrictedAccess(b *bool) *UserUpdate { return uu } +// ClearHasRestrictedAccess clears the value of the "has_restricted_access" field. +func (uu *UserUpdate) ClearHasRestrictedAccess() *UserUpdate { + uu.mutation.ClearHasRestrictedAccess() + return uu +} + // AddMembershipIDs adds the "memberships" edge to the Membership entity by IDs. func (uu *UserUpdate) AddMembershipIDs(ids ...uuid.UUID) *UserUpdate { uu.mutation.AddMembershipIDs(ids...) @@ -160,6 +166,9 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := uu.mutation.HasRestrictedAccess(); ok { _spec.SetField(user.FieldHasRestrictedAccess, field.TypeBool, value) } + if uu.mutation.HasRestrictedAccessCleared() { + _spec.ClearField(user.FieldHasRestrictedAccess, field.TypeBool) + } if uu.mutation.MembershipsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -255,6 +264,12 @@ func (uuo *UserUpdateOne) SetNillableHasRestrictedAccess(b *bool) *UserUpdateOne return uuo } +// ClearHasRestrictedAccess clears the value of the "has_restricted_access" field. +func (uuo *UserUpdateOne) ClearHasRestrictedAccess() *UserUpdateOne { + uuo.mutation.ClearHasRestrictedAccess() + return uuo +} + // AddMembershipIDs adds the "memberships" edge to the Membership entity by IDs. func (uuo *UserUpdateOne) AddMembershipIDs(ids ...uuid.UUID) *UserUpdateOne { uuo.mutation.AddMembershipIDs(ids...) @@ -387,6 +402,9 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) if value, ok := uuo.mutation.HasRestrictedAccess(); ok { _spec.SetField(user.FieldHasRestrictedAccess, field.TypeBool, value) } + if uuo.mutation.HasRestrictedAccessCleared() { + _spec.ClearField(user.FieldHasRestrictedAccess, field.TypeBool) + } if uuo.mutation.MembershipsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/app/controlplane/pkg/data/user.go b/app/controlplane/pkg/data/user.go index 68aebc3b9..cfa5fde89 100644 --- a/app/controlplane/pkg/data/user.go +++ b/app/controlplane/pkg/data/user.go @@ -127,19 +127,6 @@ func (r *userRepo) CountUsersWithRestrictedAccess(ctx context.Context) (int, err Count(ctx) } -// UpdateAllUsersAccess updates the access restriction for all users -func (r *userRepo) UpdateAllUsersAccess(ctx context.Context, isAccessRestricted bool) error { - _, err := r.data.DB.User.Update(). - Where(user.HasRestrictedAccess(!isAccessRestricted)). - SetHasRestrictedAccess(isAccessRestricted). - Save(ctx) - if err != nil { - return fmt.Errorf("error updating all users access: %w", err) - } - - return nil -} - func entUserToBizUser(eu *ent.User) *biz.User { return &biz.User{ Email: eu.Email, From 3265fb879a0a30c6c994f5ee5b70706fbaeb6d5c Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 27 Mar 2025 13:58:12 +0100 Subject: [PATCH 4/4] modify query Signed-off-by: Javier Rodriguez --- app/controlplane/pkg/biz/user.go | 2 +- app/controlplane/pkg/biz/useraccess_syncer.go | 4 ++-- app/controlplane/pkg/data/user.go | 11 ++++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/controlplane/pkg/biz/user.go b/app/controlplane/pkg/biz/user.go index 65bc7b3da..6ebe8d699 100644 --- a/app/controlplane/pkg/biz/user.go +++ b/app/controlplane/pkg/biz/user.go @@ -46,7 +46,7 @@ type UserRepo interface { Delete(ctx context.Context, userID uuid.UUID) error FindAll(ctx context.Context, pagination *pagination.OffsetPaginationOpts) ([]*User, int, error) UpdateAccess(ctx context.Context, userID uuid.UUID, isAccessRestricted bool) error - CountUsersWithRestrictedAccess(ctx context.Context) (int, error) + CountUsersWithRestrictedOrUnsetAccess(ctx context.Context) (int, error) } type UserOrgFinder interface { diff --git a/app/controlplane/pkg/biz/useraccess_syncer.go b/app/controlplane/pkg/biz/useraccess_syncer.go index fe5735b24..d0e64415e 100644 --- a/app/controlplane/pkg/biz/useraccess_syncer.go +++ b/app/controlplane/pkg/biz/useraccess_syncer.go @@ -69,8 +69,8 @@ func (u *UserAccessSyncerUseCase) StartSyncingUserAccess(ctx context.Context) er // updateUserAccessBasedOnAllowList updates the access restriction status of all users based on the allowlist func (u *UserAccessSyncerUseCase) updateUserAccessBasedOnAllowList(ctx context.Context) error { - // Count the number of users with restricted access - usersWithRestrictedAccess, err := u.userRepo.CountUsersWithRestrictedAccess(ctx) + // Count the number of users with restricted access or brand-new users where its access has not been set yet + usersWithRestrictedAccess, err := u.userRepo.CountUsersWithRestrictedOrUnsetAccess(ctx) if err != nil { return fmt.Errorf("count users with access: %w", err) } diff --git a/app/controlplane/pkg/data/user.go b/app/controlplane/pkg/data/user.go index cfa5fde89..57751d490 100644 --- a/app/controlplane/pkg/data/user.go +++ b/app/controlplane/pkg/data/user.go @@ -120,10 +120,15 @@ func (r *userRepo) FindAll(ctx context.Context, pagination *pagination.OffsetPag return result, count, nil } -// CountUsersWithRestrictedAccess returns the number of users with restricted access -func (r *userRepo) CountUsersWithRestrictedAccess(ctx context.Context) (int, error) { +// CountUsersWithRestrictedOrUnsetAccess returns the number of users with restricted access or unset access +func (r *userRepo) CountUsersWithRestrictedOrUnsetAccess(ctx context.Context) (int, error) { return r.data.DB.User.Query(). - Where(user.HasRestrictedAccess(true)). + Where( + user.Or( + user.HasRestrictedAccess(true), + user.HasRestrictedAccessIsNil(), + ), + ). Count(ctx) }