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..6ebe8d699 100644 --- a/app/controlplane/pkg/biz/user.go +++ b/app/controlplane/pkg/biz/user.go @@ -26,14 +26,17 @@ 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" ) type User struct { - ID string - Email string - CreatedAt *time.Time + ID string + Email string + CreatedAt *time.Time + HasRestrictedAccess bool } type UserRepo interface { @@ -41,6 +44,9 @@ 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 + 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 new file mode 100644 index 000000000..d0e64415e --- /dev/null +++ b/app/controlplane/pkg/biz/useraccess_syncer.go @@ -0,0 +1,192 @@ +// +// 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") + + // Update the access restriction status of all users based on the allowlist + if err := u.updateUserAccessBasedOnAllowList(ctx); 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) error { + // 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) + } + + // 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 +} + +// 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) + } + + for _, user := range users { + if err := u.updateUserAccessRestriction(ctx, user); 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) error { + isAllowListDeactivated := u.allowList == nil || len(u.allowList.GetRules()) == 0 + + var isAccessRestricted bool + + // 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) + 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/20250327120254.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250327120254.sql new file mode 100644 index 000000000..2278d8829 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250327120254.sql @@ -0,0 +1,4 @@ +-- Modify "users" table +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"); diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 95613d38e..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:YVPucsMOwyVrKFmOgfjfQ4xLEyqpppOJ7CNEXsVjiTo= +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,3 +79,4 @@ h1:YVPucsMOwyVrKFmOgfjfQ4xLEyqpppOJ7CNEXsVjiTo= 20250203084822.sql h1:xKQ2szI/uaQjz9mOthOE5SFO8wV/maJglRfciXjb+P8= 20250303153626.sql h1:y38iNqTO+lutsb2hPu+gepPDgSDmsTcWbgu7kMpbIzE= 20250326110627.sql h1:kTneMHSqpE7I8Gl88jjTy2olXpdg/np0yA45lqIxBic= +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 f7ca11774..27f0754d4 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -423,12 +423,20 @@ 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, Nullable: 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]}, + }, + }, } // 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..056d1b6cb 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,55 @@ 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 +} + +// 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. func (m *UserMutation) AddMembershipIDs(ids ...uuid.UUID) { if m.memberships == nil { @@ -10419,13 +10469,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 +10491,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 +10506,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 +10531,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) } @@ -10503,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 @@ -10516,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) } @@ -10529,6 +10602,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/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..ebd915709 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").Optional(), } } @@ -56,3 +59,9 @@ 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"), + } +} diff --git a/app/controlplane/pkg/data/ent/user.go b/app/controlplane/pkg/data/ent/user.go index d87a4b428..ff375608c 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"` + // 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. 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..65038739b 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). @@ -76,6 +79,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..1c8746fee 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,26 @@ 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)) +} + +// 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 29c562b45..fb29b51cc 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) @@ -176,6 +190,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 +274,24 @@ 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 +} + +// 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: // @@ -321,6 +357,27 @@ 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() + }) +} + +// 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 { @@ -553,6 +610,27 @@ 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() + }) +} + +// 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 f7794bbae..dcd508051 100644 --- a/app/controlplane/pkg/data/ent/user_update.go +++ b/app/controlplane/pkg/data/ent/user_update.go @@ -44,6 +44,26 @@ 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 +} + +// 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...) @@ -143,6 +163,12 @@ 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.HasRestrictedAccessCleared() { + _spec.ClearField(user.FieldHasRestrictedAccess, field.TypeBool) + } if uu.mutation.MembershipsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -224,6 +250,26 @@ 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 +} + +// 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...) @@ -353,6 +399,12 @@ 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.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 57de5ade9..57751d490 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,63 @@ 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 +} + +// 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.Or( + user.HasRestrictedAccess(true), + user.HasRestrictedAccessIsNil(), + ), + ). + Count(ctx) +} + 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, + } }