diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index d651978d0..d9cee0b0e 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -90,6 +90,11 @@ const ( // RoleGroupMaintainer is a role that can manage groups in an organization. RoleGroupMaintainer Role = "role:group:maintainer" + + // Product roles + + RoleProductViewer Role = "role:product:viewer" + RoleProductAdmin Role = "role:product:admin" ) // ManagedResources are the resources that are managed by Chainloop, considered during permissions sync @@ -443,6 +448,8 @@ func (Role) Values() (roles []string) { RoleProjectAdmin, RoleProjectViewer, RoleGroupMaintainer, + RoleProductAdmin, + RoleProductViewer, } { roles = append(roles, string(s)) } diff --git a/app/controlplane/pkg/authz/membership.go b/app/controlplane/pkg/authz/membership.go index 2006689e8..d7d876d4d 100644 --- a/app/controlplane/pkg/authz/membership.go +++ b/app/controlplane/pkg/authz/membership.go @@ -27,6 +27,7 @@ const ( ResourceTypeOrganization ResourceType = "organization" ResourceTypeProject ResourceType = "project" + ResourceTypeProduct ResourceType = "product" ResourceTypeGroup ResourceType = "group" ) @@ -46,6 +47,7 @@ func (ResourceType) Values() (values []string) { string(ResourceTypeOrganization), string(ResourceTypeProject), string(ResourceTypeGroup), + string(ResourceTypeProduct), ) return diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 1faf2af90..e6e3347d4 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -40,6 +40,7 @@ type Membership struct { MemberID uuid.UUID ResourceType authz.ResourceType ResourceID uuid.UUID + ParentID *uuid.UUID } // ListByOrgOpts are the options to filter memberships of an organization @@ -74,7 +75,7 @@ type MembershipRepo interface { // ListGroupMembershipsByUser returns all memberships of the users inherited from groups ListGroupMembershipsByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) ListAllByResource(ctx context.Context, rt authz.ResourceType, id uuid.UUID) ([]*Membership, error) - AddResourceRole(ctx context.Context, orgID uuid.UUID, resourceType authz.ResourceType, resID uuid.UUID, mType authz.MembershipType, memberID uuid.UUID, role authz.Role) error + AddResourceRole(ctx context.Context, orgID uuid.UUID, resourceType authz.ResourceType, resID uuid.UUID, mType authz.MembershipType, memberID uuid.UUID, role authz.Role, parentID *uuid.UUID) error } type MembershipsRBAC interface { @@ -409,7 +410,7 @@ func (uc *MembershipUseCase) SetProjectOwner(ctx context.Context, orgID, project } } - if err = uc.repo.AddResourceRole(ctx, orgID, authz.ResourceTypeProject, projectID, authz.MembershipTypeUser, userID, authz.RoleProjectAdmin); err != nil { + if err = uc.repo.AddResourceRole(ctx, orgID, authz.ResourceTypeProject, projectID, authz.MembershipTypeUser, userID, authz.RoleProjectAdmin, nil); err != nil { return fmt.Errorf("failed to set project owner: %w", err) } diff --git a/app/controlplane/pkg/data/ent/client.go b/app/controlplane/pkg/data/ent/client.go index e1ed0c1c7..6cfb85ae1 100644 --- a/app/controlplane/pkg/data/ent/client.go +++ b/app/controlplane/pkg/data/ent/client.go @@ -1842,6 +1842,38 @@ func (c *MembershipClient) QueryUser(m *Membership) *UserQuery { return query } +// QueryParent queries the parent edge of a Membership. +func (c *MembershipClient) QueryParent(m *Membership) *MembershipQuery { + query := (&MembershipClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := m.ID + step := sqlgraph.NewStep( + sqlgraph.From(membership.Table, membership.FieldID, id), + sqlgraph.To(membership.Table, membership.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, membership.ParentTable, membership.ParentColumn), + ) + fromV = sqlgraph.Neighbors(m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryChildren queries the children edge of a Membership. +func (c *MembershipClient) QueryChildren(m *Membership) *MembershipQuery { + query := (&MembershipClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := m.ID + step := sqlgraph.NewStep( + sqlgraph.From(membership.Table, membership.FieldID, id), + sqlgraph.To(membership.Table, membership.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, membership.ChildrenTable, membership.ChildrenColumn), + ) + fromV = sqlgraph.Neighbors(m.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *MembershipClient) Hooks() []Hook { return c.hooks.Membership diff --git a/app/controlplane/pkg/data/ent/membership.go b/app/controlplane/pkg/data/ent/membership.go index 871176a99..4527b29c8 100644 --- a/app/controlplane/pkg/data/ent/membership.go +++ b/app/controlplane/pkg/data/ent/membership.go @@ -37,6 +37,8 @@ type Membership struct { ResourceType authz.ResourceType `json:"resource_type,omitempty"` // ResourceID holds the value of the "resource_id" field. ResourceID uuid.UUID `json:"resource_id,omitempty"` + // ParentID holds the value of the "parent_id" field. + ParentID *uuid.UUID `json:"parent_id,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the MembershipQuery when eager-loading is set. Edges MembershipEdges `json:"edges"` @@ -51,9 +53,13 @@ type MembershipEdges struct { Organization *Organization `json:"organization,omitempty"` // User holds the value of the user edge. User *User `json:"user,omitempty"` + // Parent holds the value of the parent edge. + Parent *Membership `json:"parent,omitempty"` + // Children holds the value of the children edge. + Children []*Membership `json:"children,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [2]bool + loadedTypes [4]bool } // OrganizationOrErr returns the Organization value or an error if the edge @@ -78,11 +84,33 @@ func (e MembershipEdges) UserOrErr() (*User, error) { return nil, &NotLoadedError{edge: "user"} } +// ParentOrErr returns the Parent value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e MembershipEdges) ParentOrErr() (*Membership, error) { + if e.Parent != nil { + return e.Parent, nil + } else if e.loadedTypes[2] { + return nil, &NotFoundError{label: membership.Label} + } + return nil, &NotLoadedError{edge: "parent"} +} + +// ChildrenOrErr returns the Children value or an error if the edge +// was not loaded in eager-loading. +func (e MembershipEdges) ChildrenOrErr() ([]*Membership, error) { + if e.loadedTypes[3] { + return e.Children, nil + } + return nil, &NotLoadedError{edge: "children"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*Membership) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { + case membership.FieldParentID: + values[i] = &sql.NullScanner{S: new(uuid.UUID)} case membership.FieldCurrent: values[i] = new(sql.NullBool) case membership.FieldRole, membership.FieldMembershipType, membership.FieldResourceType: @@ -164,6 +192,13 @@ func (m *Membership) assignValues(columns []string, values []any) error { } else if value != nil { m.ResourceID = *value } + case membership.FieldParentID: + if value, ok := values[i].(*sql.NullScanner); !ok { + return fmt.Errorf("unexpected type %T for field parent_id", values[i]) + } else if value.Valid { + m.ParentID = new(uuid.UUID) + *m.ParentID = *value.S.(*uuid.UUID) + } case membership.ForeignKeys[0]: if value, ok := values[i].(*sql.NullScanner); !ok { return fmt.Errorf("unexpected type %T for field organization_memberships", values[i]) @@ -201,6 +236,16 @@ func (m *Membership) QueryUser() *UserQuery { return NewMembershipClient(m.config).QueryUser(m) } +// QueryParent queries the "parent" edge of the Membership entity. +func (m *Membership) QueryParent() *MembershipQuery { + return NewMembershipClient(m.config).QueryParent(m) +} + +// QueryChildren queries the "children" edge of the Membership entity. +func (m *Membership) QueryChildren() *MembershipQuery { + return NewMembershipClient(m.config).QueryChildren(m) +} + // Update returns a builder for updating this Membership. // Note that you need to call Membership.Unwrap() before calling this method if this Membership // was returned from a transaction, and the transaction was committed or rolled back. @@ -247,6 +292,11 @@ func (m *Membership) String() string { builder.WriteString(", ") builder.WriteString("resource_id=") builder.WriteString(fmt.Sprintf("%v", m.ResourceID)) + builder.WriteString(", ") + if v := m.ParentID; v != nil { + builder.WriteString("parent_id=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } builder.WriteByte(')') return builder.String() } diff --git a/app/controlplane/pkg/data/ent/membership/membership.go b/app/controlplane/pkg/data/ent/membership/membership.go index 30a4b25f9..c29b70c97 100644 --- a/app/controlplane/pkg/data/ent/membership/membership.go +++ b/app/controlplane/pkg/data/ent/membership/membership.go @@ -33,10 +33,16 @@ const ( FieldResourceType = "resource_type" // FieldResourceID holds the string denoting the resource_id field in the database. FieldResourceID = "resource_id" + // FieldParentID holds the string denoting the parent_id field in the database. + FieldParentID = "parent_id" // EdgeOrganization holds the string denoting the organization edge name in mutations. EdgeOrganization = "organization" // EdgeUser holds the string denoting the user edge name in mutations. EdgeUser = "user" + // EdgeParent holds the string denoting the parent edge name in mutations. + EdgeParent = "parent" + // EdgeChildren holds the string denoting the children edge name in mutations. + EdgeChildren = "children" // Table holds the table name of the membership in the database. Table = "memberships" // OrganizationTable is the table that holds the organization relation/edge. @@ -53,6 +59,14 @@ const ( UserInverseTable = "users" // UserColumn is the table column denoting the user relation/edge. UserColumn = "user_memberships" + // ParentTable is the table that holds the parent relation/edge. + ParentTable = "memberships" + // ParentColumn is the table column denoting the parent relation/edge. + ParentColumn = "parent_id" + // ChildrenTable is the table that holds the children relation/edge. + ChildrenTable = "memberships" + // ChildrenColumn is the table column denoting the children relation/edge. + ChildrenColumn = "parent_id" ) // Columns holds all SQL columns for membership fields. @@ -66,6 +80,7 @@ var Columns = []string{ FieldMemberID, FieldResourceType, FieldResourceID, + FieldParentID, } // ForeignKeys holds the SQL foreign-keys that are owned by the "memberships" @@ -104,7 +119,7 @@ var ( // RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. func RoleValidator(r authz.Role) error { switch r { - case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer": + case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer", "role:product:admin", "role:product:viewer": return nil default: return fmt.Errorf("membership: invalid enum value for role field: %q", r) @@ -124,7 +139,7 @@ func MembershipTypeValidator(mt authz.MembershipType) error { // ResourceTypeValidator is a validator for the "resource_type" field enum values. It is called by the builders before save. func ResourceTypeValidator(rt authz.ResourceType) error { switch rt { - case "organization", "project", "group": + case "organization", "project", "group", "product": return nil default: return fmt.Errorf("membership: invalid enum value for resource_type field: %q", rt) @@ -179,6 +194,11 @@ func ByResourceID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldResourceID, opts...).ToFunc() } +// ByParentID orders the results by the parent_id field. +func ByParentID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldParentID, opts...).ToFunc() +} + // ByOrganizationField orders the results by organization field. func ByOrganizationField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { @@ -192,6 +212,27 @@ func ByUserField(field string, opts ...sql.OrderTermOption) OrderOption { sqlgraph.OrderByNeighborTerms(s, newUserStep(), sql.OrderByField(field, opts...)) } } + +// ByParentField orders the results by parent field. +func ByParentField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newParentStep(), sql.OrderByField(field, opts...)) + } +} + +// ByChildrenCount orders the results by children count. +func ByChildrenCount(opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborsCount(s, newChildrenStep(), opts...) + } +} + +// ByChildren orders the results by children terms. +func ByChildren(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newChildrenStep(), append([]sql.OrderTerm{term}, terms...)...) + } +} func newOrganizationStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), @@ -206,3 +247,17 @@ func newUserStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn), ) } +func newParentStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, ParentTable, ParentColumn), + ) +} +func newChildrenStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(Table, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, ChildrenTable, ChildrenColumn), + ) +} diff --git a/app/controlplane/pkg/data/ent/membership/where.go b/app/controlplane/pkg/data/ent/membership/where.go index 4fc5011da..d821a66e3 100644 --- a/app/controlplane/pkg/data/ent/membership/where.go +++ b/app/controlplane/pkg/data/ent/membership/where.go @@ -82,6 +82,11 @@ func ResourceID(v uuid.UUID) predicate.Membership { return predicate.Membership(sql.FieldEQ(FieldResourceID, v)) } +// ParentID applies equality check predicate on the "parent_id" field. It's identical to ParentIDEQ. +func ParentID(v uuid.UUID) predicate.Membership { + return predicate.Membership(sql.FieldEQ(FieldParentID, v)) +} + // CurrentEQ applies the EQ predicate on the "current" field. func CurrentEQ(v bool) predicate.Membership { return predicate.Membership(sql.FieldEQ(FieldCurrent, v)) @@ -382,6 +387,36 @@ func ResourceIDNotNil() predicate.Membership { return predicate.Membership(sql.FieldNotNull(FieldResourceID)) } +// ParentIDEQ applies the EQ predicate on the "parent_id" field. +func ParentIDEQ(v uuid.UUID) predicate.Membership { + return predicate.Membership(sql.FieldEQ(FieldParentID, v)) +} + +// ParentIDNEQ applies the NEQ predicate on the "parent_id" field. +func ParentIDNEQ(v uuid.UUID) predicate.Membership { + return predicate.Membership(sql.FieldNEQ(FieldParentID, v)) +} + +// ParentIDIn applies the In predicate on the "parent_id" field. +func ParentIDIn(vs ...uuid.UUID) predicate.Membership { + return predicate.Membership(sql.FieldIn(FieldParentID, vs...)) +} + +// ParentIDNotIn applies the NotIn predicate on the "parent_id" field. +func ParentIDNotIn(vs ...uuid.UUID) predicate.Membership { + return predicate.Membership(sql.FieldNotIn(FieldParentID, vs...)) +} + +// ParentIDIsNil applies the IsNil predicate on the "parent_id" field. +func ParentIDIsNil() predicate.Membership { + return predicate.Membership(sql.FieldIsNull(FieldParentID)) +} + +// ParentIDNotNil applies the NotNil predicate on the "parent_id" field. +func ParentIDNotNil() predicate.Membership { + return predicate.Membership(sql.FieldNotNull(FieldParentID)) +} + // HasOrganization applies the HasEdge predicate on the "organization" edge. func HasOrganization() predicate.Membership { return predicate.Membership(func(s *sql.Selector) { @@ -428,6 +463,52 @@ func HasUserWith(preds ...predicate.User) predicate.Membership { }) } +// HasParent applies the HasEdge predicate on the "parent" edge. +func HasParent() predicate.Membership { + return predicate.Membership(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, ParentTable, ParentColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasParentWith applies the HasEdge predicate on the "parent" edge with a given conditions (other predicates). +func HasParentWith(preds ...predicate.Membership) predicate.Membership { + return predicate.Membership(func(s *sql.Selector) { + step := newParentStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + +// HasChildren applies the HasEdge predicate on the "children" edge. +func HasChildren() predicate.Membership { + return predicate.Membership(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, ChildrenTable, ChildrenColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasChildrenWith applies the HasEdge predicate on the "children" edge with a given conditions (other predicates). +func HasChildrenWith(preds ...predicate.Membership) predicate.Membership { + return predicate.Membership(func(s *sql.Selector) { + step := newChildrenStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.Membership) predicate.Membership { return predicate.Membership(sql.AndPredicates(predicates...)) diff --git a/app/controlplane/pkg/data/ent/membership_create.go b/app/controlplane/pkg/data/ent/membership_create.go index 2724a655a..20d3d9c0b 100644 --- a/app/controlplane/pkg/data/ent/membership_create.go +++ b/app/controlplane/pkg/data/ent/membership_create.go @@ -131,6 +131,20 @@ func (mc *MembershipCreate) SetNillableResourceID(u *uuid.UUID) *MembershipCreat return mc } +// SetParentID sets the "parent_id" field. +func (mc *MembershipCreate) SetParentID(u uuid.UUID) *MembershipCreate { + mc.mutation.SetParentID(u) + return mc +} + +// SetNillableParentID sets the "parent_id" field if the given value is not nil. +func (mc *MembershipCreate) SetNillableParentID(u *uuid.UUID) *MembershipCreate { + if u != nil { + mc.SetParentID(*u) + } + return mc +} + // SetID sets the "id" field. func (mc *MembershipCreate) SetID(u uuid.UUID) *MembershipCreate { mc.mutation.SetID(u) @@ -183,6 +197,26 @@ func (mc *MembershipCreate) SetUser(u *User) *MembershipCreate { return mc.SetUserID(u.ID) } +// SetParent sets the "parent" edge to the Membership entity. +func (mc *MembershipCreate) SetParent(m *Membership) *MembershipCreate { + return mc.SetParentID(m.ID) +} + +// AddChildIDs adds the "children" edge to the Membership entity by IDs. +func (mc *MembershipCreate) AddChildIDs(ids ...uuid.UUID) *MembershipCreate { + mc.mutation.AddChildIDs(ids...) + return mc +} + +// AddChildren adds the "children" edges to the Membership entity. +func (mc *MembershipCreate) AddChildren(m ...*Membership) *MembershipCreate { + ids := make([]uuid.UUID, len(m)) + for i := range m { + ids[i] = m[i].ID + } + return mc.AddChildIDs(ids...) +} + // Mutation returns the MembershipMutation object of the builder. func (mc *MembershipCreate) Mutation() *MembershipMutation { return mc.mutation @@ -367,6 +401,39 @@ func (mc *MembershipCreate) createSpec() (*Membership, *sqlgraph.CreateSpec) { _node.user_memberships = &nodes[0] _spec.Edges = append(_spec.Edges, edge) } + if nodes := mc.mutation.ParentIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: membership.ParentTable, + Columns: []string{membership.ParentColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.ParentID = &nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } + if nodes := mc.mutation.ChildrenIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: membership.ChildrenTable, + Columns: []string{membership.ChildrenColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } @@ -527,6 +594,24 @@ func (u *MembershipUpsert) ClearResourceID() *MembershipUpsert { return u } +// SetParentID sets the "parent_id" field. +func (u *MembershipUpsert) SetParentID(v uuid.UUID) *MembershipUpsert { + u.Set(membership.FieldParentID, v) + return u +} + +// UpdateParentID sets the "parent_id" field to the value that was provided on create. +func (u *MembershipUpsert) UpdateParentID() *MembershipUpsert { + u.SetExcluded(membership.FieldParentID) + return u +} + +// ClearParentID clears the value of the "parent_id" field. +func (u *MembershipUpsert) ClearParentID() *MembershipUpsert { + u.SetNull(membership.FieldParentID) + 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: // @@ -704,6 +789,27 @@ func (u *MembershipUpsertOne) ClearResourceID() *MembershipUpsertOne { }) } +// SetParentID sets the "parent_id" field. +func (u *MembershipUpsertOne) SetParentID(v uuid.UUID) *MembershipUpsertOne { + return u.Update(func(s *MembershipUpsert) { + s.SetParentID(v) + }) +} + +// UpdateParentID sets the "parent_id" field to the value that was provided on create. +func (u *MembershipUpsertOne) UpdateParentID() *MembershipUpsertOne { + return u.Update(func(s *MembershipUpsert) { + s.UpdateParentID() + }) +} + +// ClearParentID clears the value of the "parent_id" field. +func (u *MembershipUpsertOne) ClearParentID() *MembershipUpsertOne { + return u.Update(func(s *MembershipUpsert) { + s.ClearParentID() + }) +} + // Exec executes the query. func (u *MembershipUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -1048,6 +1154,27 @@ func (u *MembershipUpsertBulk) ClearResourceID() *MembershipUpsertBulk { }) } +// SetParentID sets the "parent_id" field. +func (u *MembershipUpsertBulk) SetParentID(v uuid.UUID) *MembershipUpsertBulk { + return u.Update(func(s *MembershipUpsert) { + s.SetParentID(v) + }) +} + +// UpdateParentID sets the "parent_id" field to the value that was provided on create. +func (u *MembershipUpsertBulk) UpdateParentID() *MembershipUpsertBulk { + return u.Update(func(s *MembershipUpsert) { + s.UpdateParentID() + }) +} + +// ClearParentID clears the value of the "parent_id" field. +func (u *MembershipUpsertBulk) ClearParentID() *MembershipUpsertBulk { + return u.Update(func(s *MembershipUpsert) { + s.ClearParentID() + }) +} + // Exec executes the query. func (u *MembershipUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/app/controlplane/pkg/data/ent/membership_query.go b/app/controlplane/pkg/data/ent/membership_query.go index 87033f9b5..15ac2fd3c 100644 --- a/app/controlplane/pkg/data/ent/membership_query.go +++ b/app/controlplane/pkg/data/ent/membership_query.go @@ -4,6 +4,7 @@ package ent import ( "context" + "database/sql/driver" "fmt" "math" @@ -28,6 +29,8 @@ type MembershipQuery struct { predicates []predicate.Membership withOrganization *OrganizationQuery withUser *UserQuery + withParent *MembershipQuery + withChildren *MembershipQuery withFKs bool modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). @@ -110,6 +113,50 @@ func (mq *MembershipQuery) QueryUser() *UserQuery { return query } +// QueryParent chains the current query on the "parent" edge. +func (mq *MembershipQuery) QueryParent() *MembershipQuery { + query := (&MembershipClient{config: mq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := mq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := mq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(membership.Table, membership.FieldID, selector), + sqlgraph.To(membership.Table, membership.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, membership.ParentTable, membership.ParentColumn), + ) + fromU = sqlgraph.SetNeighbors(mq.driver.Dialect(), step) + return fromU, nil + } + return query +} + +// QueryChildren chains the current query on the "children" edge. +func (mq *MembershipQuery) QueryChildren() *MembershipQuery { + query := (&MembershipClient{config: mq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := mq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := mq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(membership.Table, membership.FieldID, selector), + sqlgraph.To(membership.Table, membership.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, membership.ChildrenTable, membership.ChildrenColumn), + ) + fromU = sqlgraph.SetNeighbors(mq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first Membership entity from the query. // Returns a *NotFoundError when no Membership was found. func (mq *MembershipQuery) First(ctx context.Context) (*Membership, error) { @@ -304,6 +351,8 @@ func (mq *MembershipQuery) Clone() *MembershipQuery { predicates: append([]predicate.Membership{}, mq.predicates...), withOrganization: mq.withOrganization.Clone(), withUser: mq.withUser.Clone(), + withParent: mq.withParent.Clone(), + withChildren: mq.withChildren.Clone(), // clone intermediate query. sql: mq.sql.Clone(), path: mq.path, @@ -333,6 +382,28 @@ func (mq *MembershipQuery) WithUser(opts ...func(*UserQuery)) *MembershipQuery { return mq } +// WithParent tells the query-builder to eager-load the nodes that are connected to +// the "parent" edge. The optional arguments are used to configure the query builder of the edge. +func (mq *MembershipQuery) WithParent(opts ...func(*MembershipQuery)) *MembershipQuery { + query := (&MembershipClient{config: mq.config}).Query() + for _, opt := range opts { + opt(query) + } + mq.withParent = query + return mq +} + +// WithChildren tells the query-builder to eager-load the nodes that are connected to +// the "children" edge. The optional arguments are used to configure the query builder of the edge. +func (mq *MembershipQuery) WithChildren(opts ...func(*MembershipQuery)) *MembershipQuery { + query := (&MembershipClient{config: mq.config}).Query() + for _, opt := range opts { + opt(query) + } + mq.withChildren = query + return mq +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -412,9 +483,11 @@ func (mq *MembershipQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*M nodes = []*Membership{} withFKs = mq.withFKs _spec = mq.querySpec() - loadedTypes = [2]bool{ + loadedTypes = [4]bool{ mq.withOrganization != nil, mq.withUser != nil, + mq.withParent != nil, + mq.withChildren != nil, } ) if mq.withOrganization != nil || mq.withUser != nil { @@ -456,6 +529,19 @@ func (mq *MembershipQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*M return nil, err } } + if query := mq.withParent; query != nil { + if err := mq.loadParent(ctx, query, nodes, nil, + func(n *Membership, e *Membership) { n.Edges.Parent = e }); err != nil { + return nil, err + } + } + if query := mq.withChildren; query != nil { + if err := mq.loadChildren(ctx, query, nodes, + func(n *Membership) { n.Edges.Children = []*Membership{} }, + func(n *Membership, e *Membership) { n.Edges.Children = append(n.Edges.Children, e) }); err != nil { + return nil, err + } + } return nodes, nil } @@ -523,6 +609,72 @@ func (mq *MembershipQuery) loadUser(ctx context.Context, query *UserQuery, nodes } return nil } +func (mq *MembershipQuery) loadParent(ctx context.Context, query *MembershipQuery, nodes []*Membership, init func(*Membership), assign func(*Membership, *Membership)) error { + ids := make([]uuid.UUID, 0, len(nodes)) + nodeids := make(map[uuid.UUID][]*Membership) + for i := range nodes { + if nodes[i].ParentID == nil { + continue + } + fk := *nodes[i].ParentID + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(membership.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "parent_id" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} +func (mq *MembershipQuery) loadChildren(ctx context.Context, query *MembershipQuery, nodes []*Membership, init func(*Membership), assign func(*Membership, *Membership)) error { + fks := make([]driver.Value, 0, len(nodes)) + nodeids := make(map[uuid.UUID]*Membership) + for i := range nodes { + fks = append(fks, nodes[i].ID) + nodeids[nodes[i].ID] = nodes[i] + if init != nil { + init(nodes[i]) + } + } + query.withFKs = true + if len(query.ctx.Fields) > 0 { + query.ctx.AppendFieldOnce(membership.FieldParentID) + } + query.Where(predicate.Membership(func(s *sql.Selector) { + s.Where(sql.InValues(s.C(membership.ChildrenColumn), fks...)) + })) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + fk := n.ParentID + if fk == nil { + return fmt.Errorf(`foreign-key "parent_id" is nil for node %v`, n.ID) + } + node, ok := nodeids[*fk] + if !ok { + return fmt.Errorf(`unexpected referenced foreign-key "parent_id" returned %v for node %v`, *fk, n.ID) + } + assign(node, n) + } + return nil +} func (mq *MembershipQuery) sqlCount(ctx context.Context) (int, error) { _spec := mq.querySpec() @@ -552,6 +704,9 @@ func (mq *MembershipQuery) querySpec() *sqlgraph.QuerySpec { _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) } } + if mq.withParent != nil { + _spec.Node.AddColumnOnce(membership.FieldParentID) + } } if ps := mq.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/membership_update.go b/app/controlplane/pkg/data/ent/membership_update.go index b0afedb85..81a15ef58 100644 --- a/app/controlplane/pkg/data/ent/membership_update.go +++ b/app/controlplane/pkg/data/ent/membership_update.go @@ -155,6 +155,26 @@ func (mu *MembershipUpdate) ClearResourceID() *MembershipUpdate { return mu } +// SetParentID sets the "parent_id" field. +func (mu *MembershipUpdate) SetParentID(u uuid.UUID) *MembershipUpdate { + mu.mutation.SetParentID(u) + return mu +} + +// SetNillableParentID sets the "parent_id" field if the given value is not nil. +func (mu *MembershipUpdate) SetNillableParentID(u *uuid.UUID) *MembershipUpdate { + if u != nil { + mu.SetParentID(*u) + } + return mu +} + +// ClearParentID clears the value of the "parent_id" field. +func (mu *MembershipUpdate) ClearParentID() *MembershipUpdate { + mu.mutation.ClearParentID() + return mu +} + // SetOrganizationID sets the "organization" edge to the Organization entity by ID. func (mu *MembershipUpdate) SetOrganizationID(id uuid.UUID) *MembershipUpdate { mu.mutation.SetOrganizationID(id) @@ -193,6 +213,26 @@ func (mu *MembershipUpdate) SetUser(u *User) *MembershipUpdate { return mu.SetUserID(u.ID) } +// SetParent sets the "parent" edge to the Membership entity. +func (mu *MembershipUpdate) SetParent(m *Membership) *MembershipUpdate { + return mu.SetParentID(m.ID) +} + +// AddChildIDs adds the "children" edge to the Membership entity by IDs. +func (mu *MembershipUpdate) AddChildIDs(ids ...uuid.UUID) *MembershipUpdate { + mu.mutation.AddChildIDs(ids...) + return mu +} + +// AddChildren adds the "children" edges to the Membership entity. +func (mu *MembershipUpdate) AddChildren(m ...*Membership) *MembershipUpdate { + ids := make([]uuid.UUID, len(m)) + for i := range m { + ids[i] = m[i].ID + } + return mu.AddChildIDs(ids...) +} + // Mutation returns the MembershipMutation object of the builder. func (mu *MembershipUpdate) Mutation() *MembershipMutation { return mu.mutation @@ -210,6 +250,33 @@ func (mu *MembershipUpdate) ClearUser() *MembershipUpdate { return mu } +// ClearParent clears the "parent" edge to the Membership entity. +func (mu *MembershipUpdate) ClearParent() *MembershipUpdate { + mu.mutation.ClearParent() + return mu +} + +// ClearChildren clears all "children" edges to the Membership entity. +func (mu *MembershipUpdate) ClearChildren() *MembershipUpdate { + mu.mutation.ClearChildren() + return mu +} + +// RemoveChildIDs removes the "children" edge to Membership entities by IDs. +func (mu *MembershipUpdate) RemoveChildIDs(ids ...uuid.UUID) *MembershipUpdate { + mu.mutation.RemoveChildIDs(ids...) + return mu +} + +// RemoveChildren removes "children" edges to Membership entities. +func (mu *MembershipUpdate) RemoveChildren(m ...*Membership) *MembershipUpdate { + ids := make([]uuid.UUID, len(m)) + for i := range m { + ids[i] = m[i].ID + } + return mu.RemoveChildIDs(ids...) +} + // Save executes the query and returns the number of nodes affected by the update operation. func (mu *MembershipUpdate) Save(ctx context.Context) (int, error) { return withHooks(ctx, mu.sqlSave, mu.mutation, mu.hooks) @@ -366,6 +433,80 @@ func (mu *MembershipUpdate) sqlSave(ctx context.Context) (n int, err error) { } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if mu.mutation.ParentCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: membership.ParentTable, + Columns: []string{membership.ParentColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := mu.mutation.ParentIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: membership.ParentTable, + Columns: []string{membership.ParentColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + if mu.mutation.ChildrenCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: membership.ChildrenTable, + Columns: []string{membership.ChildrenColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := mu.mutation.RemovedChildrenIDs(); len(nodes) > 0 && !mu.mutation.ChildrenCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: membership.ChildrenTable, + Columns: []string{membership.ChildrenColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := mu.mutation.ChildrenIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: membership.ChildrenTable, + Columns: []string{membership.ChildrenColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _spec.AddModifiers(mu.modifiers...) if n, err = sqlgraph.UpdateNodes(ctx, mu.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { @@ -510,6 +651,26 @@ func (muo *MembershipUpdateOne) ClearResourceID() *MembershipUpdateOne { return muo } +// SetParentID sets the "parent_id" field. +func (muo *MembershipUpdateOne) SetParentID(u uuid.UUID) *MembershipUpdateOne { + muo.mutation.SetParentID(u) + return muo +} + +// SetNillableParentID sets the "parent_id" field if the given value is not nil. +func (muo *MembershipUpdateOne) SetNillableParentID(u *uuid.UUID) *MembershipUpdateOne { + if u != nil { + muo.SetParentID(*u) + } + return muo +} + +// ClearParentID clears the value of the "parent_id" field. +func (muo *MembershipUpdateOne) ClearParentID() *MembershipUpdateOne { + muo.mutation.ClearParentID() + return muo +} + // SetOrganizationID sets the "organization" edge to the Organization entity by ID. func (muo *MembershipUpdateOne) SetOrganizationID(id uuid.UUID) *MembershipUpdateOne { muo.mutation.SetOrganizationID(id) @@ -548,6 +709,26 @@ func (muo *MembershipUpdateOne) SetUser(u *User) *MembershipUpdateOne { return muo.SetUserID(u.ID) } +// SetParent sets the "parent" edge to the Membership entity. +func (muo *MembershipUpdateOne) SetParent(m *Membership) *MembershipUpdateOne { + return muo.SetParentID(m.ID) +} + +// AddChildIDs adds the "children" edge to the Membership entity by IDs. +func (muo *MembershipUpdateOne) AddChildIDs(ids ...uuid.UUID) *MembershipUpdateOne { + muo.mutation.AddChildIDs(ids...) + return muo +} + +// AddChildren adds the "children" edges to the Membership entity. +func (muo *MembershipUpdateOne) AddChildren(m ...*Membership) *MembershipUpdateOne { + ids := make([]uuid.UUID, len(m)) + for i := range m { + ids[i] = m[i].ID + } + return muo.AddChildIDs(ids...) +} + // Mutation returns the MembershipMutation object of the builder. func (muo *MembershipUpdateOne) Mutation() *MembershipMutation { return muo.mutation @@ -565,6 +746,33 @@ func (muo *MembershipUpdateOne) ClearUser() *MembershipUpdateOne { return muo } +// ClearParent clears the "parent" edge to the Membership entity. +func (muo *MembershipUpdateOne) ClearParent() *MembershipUpdateOne { + muo.mutation.ClearParent() + return muo +} + +// ClearChildren clears all "children" edges to the Membership entity. +func (muo *MembershipUpdateOne) ClearChildren() *MembershipUpdateOne { + muo.mutation.ClearChildren() + return muo +} + +// RemoveChildIDs removes the "children" edge to Membership entities by IDs. +func (muo *MembershipUpdateOne) RemoveChildIDs(ids ...uuid.UUID) *MembershipUpdateOne { + muo.mutation.RemoveChildIDs(ids...) + return muo +} + +// RemoveChildren removes "children" edges to Membership entities. +func (muo *MembershipUpdateOne) RemoveChildren(m ...*Membership) *MembershipUpdateOne { + ids := make([]uuid.UUID, len(m)) + for i := range m { + ids[i] = m[i].ID + } + return muo.RemoveChildIDs(ids...) +} + // Where appends a list predicates to the MembershipUpdate builder. func (muo *MembershipUpdateOne) Where(ps ...predicate.Membership) *MembershipUpdateOne { muo.mutation.Where(ps...) @@ -751,6 +959,80 @@ func (muo *MembershipUpdateOne) sqlSave(ctx context.Context) (_node *Membership, } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if muo.mutation.ParentCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: membership.ParentTable, + Columns: []string{membership.ParentColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := muo.mutation.ParentIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: membership.ParentTable, + Columns: []string{membership.ParentColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + if muo.mutation.ChildrenCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: membership.ChildrenTable, + Columns: []string{membership.ChildrenColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := muo.mutation.RemovedChildrenIDs(); len(nodes) > 0 && !muo.mutation.ChildrenCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: membership.ChildrenTable, + Columns: []string{membership.ChildrenColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := muo.mutation.ChildrenIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: membership.ChildrenTable, + Columns: []string{membership.ChildrenColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(membership.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _spec.AddModifiers(muo.modifiers...) _node = &Membership{config: muo.config} _spec.Assign = _node.assignValues diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250723114128.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250723114128.sql new file mode 100644 index 000000000..3e2f39d86 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250723114128.sql @@ -0,0 +1,4 @@ +-- Modify "memberships" table +-- NOTE: by default, foreign keys are not generated for performance reasons in cases where we do a soft-deletion. +-- In this particular case, enabling cascade deletion makes sense to automatically remove inherited rows +ALTER TABLE "memberships" ADD COLUMN "parent_id" uuid NULL, ADD CONSTRAINT "memberships_memberships_children" FOREIGN KEY ("parent_id") REFERENCES "memberships" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250723171233.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250723171233.sql new file mode 100644 index 000000000..5851fd5a9 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250723171233.sql @@ -0,0 +1,6 @@ +-- Drop index "membership_membership_type_member_id_resource_type_resource_id" from table: "memberships" +DROP INDEX "membership_membership_type_member_id_resource_type_resource_id"; +-- Create index "membership_membership_type_mem_33e9cb590a3adfa25d916afabf657740" to table: "memberships" +CREATE UNIQUE INDEX "membership_membership_type_mem_33e9cb590a3adfa25d916afabf657740" ON "memberships" ("membership_type", "member_id", "resource_type", "resource_id", "role", "parent_id") WHERE (parent_id IS NOT NULL); +-- Create index "membership_membership_type_mem_8014883ac7acffee8425ce171cf6f4cf" to table: "memberships" +CREATE UNIQUE INDEX "membership_membership_type_mem_8014883ac7acffee8425ce171cf6f4cf" ON "memberships" ("membership_type", "member_id", "resource_type", "resource_id", "role") WHERE (parent_id IS NULL); diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index a9af79d90..ebd696d66 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:bxjg7J7Xk0+87+PhS0+F/c4AcHJZ3pkf0FnPMZa39Q0= +h1:QHFhnD3Le8WU03zeKuQWvjRaaMKX2ONDFb64ty4phZo= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -98,3 +98,5 @@ h1:bxjg7J7Xk0+87+PhS0+F/c4AcHJZ3pkf0FnPMZa39Q0= 20250710105502.sql h1:EA6Ta1qsZcrNoOrO5zUNgiweHDtjl0HUlobukRuruko= 20250714172256.sql h1:S0ImNk0sMjWVVZvS6VVHn2h96/nx8GOf4aVxELbJAcg= 20250715100956.sql h1:y9eOaPMpQTlcJppjaGzeuHBTNDwe6sGbxSVU8e7LL1o= +20250723114128.sql h1:OZDXg9CdImiwPsi7X9TJoZ4uPnSa17oY/9HjsxG7WNk= +20250723171233.sql h1:Aq4IUr4ForrwmK9jMPPtwl4V8e2plYff/IcSgIc0XFo= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index 66e91d0d3..88c973dbf 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -320,11 +320,12 @@ var ( {Name: "current", Type: field.TypeBool, Default: false}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "updated_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, - {Name: "role", Type: field.TypeEnum, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer"}}, + {Name: "role", Type: field.TypeEnum, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer", "role:product:admin", "role:product:viewer"}}, {Name: "membership_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"user", "group"}}, {Name: "member_id", Type: field.TypeUUID, Nullable: true}, - {Name: "resource_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"organization", "project", "group"}}, + {Name: "resource_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"organization", "project", "group", "product"}}, {Name: "resource_id", Type: field.TypeUUID, Nullable: true}, + {Name: "parent_id", Type: field.TypeUUID, Nullable: true}, {Name: "organization_memberships", Type: field.TypeUUID, Nullable: true}, {Name: "user_memberships", Type: field.TypeUUID, Nullable: true}, } @@ -335,14 +336,20 @@ var ( PrimaryKey: []*schema.Column{MembershipsColumns[0]}, ForeignKeys: []*schema.ForeignKey{ { - Symbol: "memberships_organizations_memberships", + Symbol: "memberships_memberships_children", Columns: []*schema.Column{MembershipsColumns[9]}, + RefColumns: []*schema.Column{MembershipsColumns[0]}, + OnDelete: schema.Cascade, + }, + { + Symbol: "memberships_organizations_memberships", + Columns: []*schema.Column{MembershipsColumns[10]}, RefColumns: []*schema.Column{OrganizationsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "memberships_users_memberships", - Columns: []*schema.Column{MembershipsColumns[10]}, + Columns: []*schema.Column{MembershipsColumns[11]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.Cascade, }, @@ -351,12 +358,23 @@ var ( { Name: "membership_organization_memberships_user_memberships", Unique: false, - Columns: []*schema.Column{MembershipsColumns[9], MembershipsColumns[10]}, + Columns: []*schema.Column{MembershipsColumns[10], MembershipsColumns[11]}, }, { - Name: "membership_membership_type_member_id_resource_type_resource_id", + Name: "membership_membership_type_member_id_resource_type_resource_id_role_parent_id", Unique: true, - Columns: []*schema.Column{MembershipsColumns[5], MembershipsColumns[6], MembershipsColumns[7], MembershipsColumns[8]}, + Columns: []*schema.Column{MembershipsColumns[5], MembershipsColumns[6], MembershipsColumns[7], MembershipsColumns[8], MembershipsColumns[4], MembershipsColumns[9]}, + Annotation: &entsql.IndexAnnotation{ + Where: "parent_id IS NOT NULL", + }, + }, + { + Name: "membership_membership_type_member_id_resource_type_resource_id_role", + Unique: true, + Columns: []*schema.Column{MembershipsColumns[5], MembershipsColumns[6], MembershipsColumns[7], MembershipsColumns[8], MembershipsColumns[4]}, + Annotation: &entsql.IndexAnnotation{ + Where: "parent_id IS NULL", + }, }, }, } @@ -367,7 +385,7 @@ var ( {Name: "status", Type: field.TypeEnum, Enums: []string{"accepted", "pending"}, Default: "pending"}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "deleted_at", Type: field.TypeTime, Nullable: true}, - {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer"}}, + {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer", "role:product:admin", "role:product:viewer"}}, {Name: "context", Type: field.TypeJSON, Nullable: true}, {Name: "organization_id", Type: field.TypeUUID}, {Name: "sender_id", Type: field.TypeUUID}, @@ -923,8 +941,9 @@ func init() { IntegrationsTable.ForeignKeys[0].RefTable = OrganizationsTable IntegrationAttachmentsTable.ForeignKeys[0].RefTable = IntegrationsTable IntegrationAttachmentsTable.ForeignKeys[1].RefTable = WorkflowsTable - MembershipsTable.ForeignKeys[0].RefTable = OrganizationsTable - MembershipsTable.ForeignKeys[1].RefTable = UsersTable + MembershipsTable.ForeignKeys[0].RefTable = MembershipsTable + MembershipsTable.ForeignKeys[1].RefTable = OrganizationsTable + MembershipsTable.ForeignKeys[2].RefTable = UsersTable OrgInvitationsTable.ForeignKeys[0].RefTable = OrganizationsTable OrgInvitationsTable.ForeignKeys[1].RefTable = UsersTable ProjectsTable.ForeignKeys[0].RefTable = OrganizationsTable diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index 4230c0b82..f8994d7d4 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -6589,6 +6589,11 @@ type MembershipMutation struct { clearedorganization bool user *uuid.UUID cleareduser bool + parent *uuid.UUID + clearedparent bool + children map[uuid.UUID]struct{} + removedchildren map[uuid.UUID]struct{} + clearedchildren bool done bool oldValue func(context.Context) (*Membership, error) predicates []predicate.Membership @@ -7038,6 +7043,55 @@ func (m *MembershipMutation) ResetResourceID() { delete(m.clearedFields, membership.FieldResourceID) } +// SetParentID sets the "parent_id" field. +func (m *MembershipMutation) SetParentID(u uuid.UUID) { + m.parent = &u +} + +// ParentID returns the value of the "parent_id" field in the mutation. +func (m *MembershipMutation) ParentID() (r uuid.UUID, exists bool) { + v := m.parent + if v == nil { + return + } + return *v, true +} + +// OldParentID returns the old "parent_id" field's value of the Membership entity. +// If the Membership 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 *MembershipMutation) OldParentID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldParentID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldParentID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldParentID: %w", err) + } + return oldValue.ParentID, nil +} + +// ClearParentID clears the value of the "parent_id" field. +func (m *MembershipMutation) ClearParentID() { + m.parent = nil + m.clearedFields[membership.FieldParentID] = struct{}{} +} + +// ParentIDCleared returns if the "parent_id" field was cleared in this mutation. +func (m *MembershipMutation) ParentIDCleared() bool { + _, ok := m.clearedFields[membership.FieldParentID] + return ok +} + +// ResetParentID resets all changes to the "parent_id" field. +func (m *MembershipMutation) ResetParentID() { + m.parent = nil + delete(m.clearedFields, membership.FieldParentID) +} + // SetOrganizationID sets the "organization" edge to the Organization entity by id. func (m *MembershipMutation) SetOrganizationID(id uuid.UUID) { m.organization = &id @@ -7116,6 +7170,87 @@ func (m *MembershipMutation) ResetUser() { m.cleareduser = false } +// ClearParent clears the "parent" edge to the Membership entity. +func (m *MembershipMutation) ClearParent() { + m.clearedparent = true + m.clearedFields[membership.FieldParentID] = struct{}{} +} + +// ParentCleared reports if the "parent" edge to the Membership entity was cleared. +func (m *MembershipMutation) ParentCleared() bool { + return m.ParentIDCleared() || m.clearedparent +} + +// ParentIDs returns the "parent" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// ParentID instead. It exists only for internal usage by the builders. +func (m *MembershipMutation) ParentIDs() (ids []uuid.UUID) { + if id := m.parent; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetParent resets all changes to the "parent" edge. +func (m *MembershipMutation) ResetParent() { + m.parent = nil + m.clearedparent = false +} + +// AddChildIDs adds the "children" edge to the Membership entity by ids. +func (m *MembershipMutation) AddChildIDs(ids ...uuid.UUID) { + if m.children == nil { + m.children = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.children[ids[i]] = struct{}{} + } +} + +// ClearChildren clears the "children" edge to the Membership entity. +func (m *MembershipMutation) ClearChildren() { + m.clearedchildren = true +} + +// ChildrenCleared reports if the "children" edge to the Membership entity was cleared. +func (m *MembershipMutation) ChildrenCleared() bool { + return m.clearedchildren +} + +// RemoveChildIDs removes the "children" edge to the Membership entity by IDs. +func (m *MembershipMutation) RemoveChildIDs(ids ...uuid.UUID) { + if m.removedchildren == nil { + m.removedchildren = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.children, ids[i]) + m.removedchildren[ids[i]] = struct{}{} + } +} + +// RemovedChildren returns the removed IDs of the "children" edge to the Membership entity. +func (m *MembershipMutation) RemovedChildrenIDs() (ids []uuid.UUID) { + for id := range m.removedchildren { + ids = append(ids, id) + } + return +} + +// ChildrenIDs returns the "children" edge IDs in the mutation. +func (m *MembershipMutation) ChildrenIDs() (ids []uuid.UUID) { + for id := range m.children { + ids = append(ids, id) + } + return +} + +// ResetChildren resets all changes to the "children" edge. +func (m *MembershipMutation) ResetChildren() { + m.children = nil + m.clearedchildren = false + m.removedchildren = nil +} + // Where appends a list predicates to the MembershipMutation builder. func (m *MembershipMutation) Where(ps ...predicate.Membership) { m.predicates = append(m.predicates, ps...) @@ -7150,7 +7285,7 @@ func (m *MembershipMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *MembershipMutation) Fields() []string { - fields := make([]string, 0, 8) + fields := make([]string, 0, 9) if m.current != nil { fields = append(fields, membership.FieldCurrent) } @@ -7175,6 +7310,9 @@ func (m *MembershipMutation) Fields() []string { if m.resource_id != nil { fields = append(fields, membership.FieldResourceID) } + if m.parent != nil { + fields = append(fields, membership.FieldParentID) + } return fields } @@ -7199,6 +7337,8 @@ func (m *MembershipMutation) Field(name string) (ent.Value, bool) { return m.ResourceType() case membership.FieldResourceID: return m.ResourceID() + case membership.FieldParentID: + return m.ParentID() } return nil, false } @@ -7224,6 +7364,8 @@ func (m *MembershipMutation) OldField(ctx context.Context, name string) (ent.Val return m.OldResourceType(ctx) case membership.FieldResourceID: return m.OldResourceID(ctx) + case membership.FieldParentID: + return m.OldParentID(ctx) } return nil, fmt.Errorf("unknown Membership field %s", name) } @@ -7289,6 +7431,13 @@ func (m *MembershipMutation) SetField(name string, value ent.Value) error { } m.SetResourceID(v) return nil + case membership.FieldParentID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetParentID(v) + return nil } return fmt.Errorf("unknown Membership field %s", name) } @@ -7331,6 +7480,9 @@ func (m *MembershipMutation) ClearedFields() []string { if m.FieldCleared(membership.FieldResourceID) { fields = append(fields, membership.FieldResourceID) } + if m.FieldCleared(membership.FieldParentID) { + fields = append(fields, membership.FieldParentID) + } return fields } @@ -7357,6 +7509,9 @@ func (m *MembershipMutation) ClearField(name string) error { case membership.FieldResourceID: m.ClearResourceID() return nil + case membership.FieldParentID: + m.ClearParentID() + return nil } return fmt.Errorf("unknown Membership nullable field %s", name) } @@ -7389,19 +7544,28 @@ func (m *MembershipMutation) ResetField(name string) error { case membership.FieldResourceID: m.ResetResourceID() return nil + case membership.FieldParentID: + m.ResetParentID() + return nil } return fmt.Errorf("unknown Membership field %s", name) } // AddedEdges returns all edge names that were set/added in this mutation. func (m *MembershipMutation) AddedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 4) if m.organization != nil { edges = append(edges, membership.EdgeOrganization) } if m.user != nil { edges = append(edges, membership.EdgeUser) } + if m.parent != nil { + edges = append(edges, membership.EdgeParent) + } + if m.children != nil { + edges = append(edges, membership.EdgeChildren) + } return edges } @@ -7417,31 +7581,58 @@ func (m *MembershipMutation) AddedIDs(name string) []ent.Value { if id := m.user; id != nil { return []ent.Value{*id} } + case membership.EdgeParent: + if id := m.parent; id != nil { + return []ent.Value{*id} + } + case membership.EdgeChildren: + ids := make([]ent.Value, 0, len(m.children)) + for id := range m.children { + ids = append(ids, id) + } + return ids } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *MembershipMutation) RemovedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 4) + if m.removedchildren != nil { + edges = append(edges, membership.EdgeChildren) + } return edges } // RemovedIDs returns all IDs (to other nodes) that were removed for the edge with // the given name in this mutation. func (m *MembershipMutation) RemovedIDs(name string) []ent.Value { + switch name { + case membership.EdgeChildren: + ids := make([]ent.Value, 0, len(m.removedchildren)) + for id := range m.removedchildren { + ids = append(ids, id) + } + return ids + } return nil } // ClearedEdges returns all edge names that were cleared in this mutation. func (m *MembershipMutation) ClearedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 4) if m.clearedorganization { edges = append(edges, membership.EdgeOrganization) } if m.cleareduser { edges = append(edges, membership.EdgeUser) } + if m.clearedparent { + edges = append(edges, membership.EdgeParent) + } + if m.clearedchildren { + edges = append(edges, membership.EdgeChildren) + } return edges } @@ -7453,6 +7644,10 @@ func (m *MembershipMutation) EdgeCleared(name string) bool { return m.clearedorganization case membership.EdgeUser: return m.cleareduser + case membership.EdgeParent: + return m.clearedparent + case membership.EdgeChildren: + return m.clearedchildren } return false } @@ -7467,6 +7662,9 @@ func (m *MembershipMutation) ClearEdge(name string) error { case membership.EdgeUser: m.ClearUser() return nil + case membership.EdgeParent: + m.ClearParent() + return nil } return fmt.Errorf("unknown Membership unique edge %s", name) } @@ -7481,6 +7679,12 @@ func (m *MembershipMutation) ResetEdge(name string) error { case membership.EdgeUser: m.ResetUser() return nil + case membership.EdgeParent: + m.ResetParent() + return nil + case membership.EdgeChildren: + m.ResetChildren() + return nil } return fmt.Errorf("unknown Membership edge %s", name) } diff --git a/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go b/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go index f5c5e4f9c..061e5cdf7 100644 --- a/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go +++ b/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go @@ -101,7 +101,7 @@ func StatusValidator(s biz.OrgInvitationStatus) error { // RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. func RoleValidator(r authz.Role) error { switch r { - case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer": + case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer", "role:product:admin", "role:product:viewer": return nil default: return fmt.Errorf("orginvitation: invalid enum value for role field: %q", r) diff --git a/app/controlplane/pkg/data/ent/schema-viz.html b/app/controlplane/pkg/data/ent/schema-viz.html index bbfda2835..3cf38b185 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\":\"last_used_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_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\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Group\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"member_count\",\"type\":\"int\"}]},{\"id\":\"GroupMembership\",\"fields\":[{\"name\":\"group_id\",\"type\":\"uuid.UUID\"},{\"name\":\"user_id\",\"type\":\"uuid.UUID\"},{\"name\":\"maintainer\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"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\"},{\"name\":\"membership_type\",\"type\":\"authz.MembershipType\"},{\"name\":\"member_id\",\"type\":\"uuid.UUID\"},{\"name\":\"resource_type\",\"type\":\"authz.ResourceType\"},{\"name\":\"resource_id\",\"type\":\"uuid.UUID\"}]},{\"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\"},{\"name\":\"context\",\"type\":\"biz.OrgInvitationContext\"}]},{\"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\":\"updated_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\"},{\"name\":\"latest\",\"type\":\"bool\"}]},{\"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\"},{\"name\":\"first_name\",\"type\":\"string\"},{\"name\":\"last_name\",\"type\":\"string\"}]},{\"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\"},{\"name\":\"scoped_resource_type\",\"type\":\"biz.ContractScope\"},{\"name\":\"scoped_resource_id\",\"type\":\"uuid.UUID\"}]},{\"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\":\"APIToken\",\"to\":\"Project\",\"label\":\"project\"},{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"CASMapping\",\"to\":\"Project\",\"label\":\"project\"},{\"from\":\"Group\",\"to\":\"User\",\"label\":\"members\"},{\"from\":\"GroupMembership\",\"to\":\"Group\",\"label\":\"group\"},{\"from\":\"GroupMembership\",\"to\":\"User\",\"label\":\"user\"},{\"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\":\"Organization\",\"to\":\"Group\",\"label\":\"groups\"},{\"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\":\"last_used_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_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\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Group\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"member_count\",\"type\":\"int\"}]},{\"id\":\"GroupMembership\",\"fields\":[{\"name\":\"group_id\",\"type\":\"uuid.UUID\"},{\"name\":\"user_id\",\"type\":\"uuid.UUID\"},{\"name\":\"maintainer\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"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\"},{\"name\":\"membership_type\",\"type\":\"authz.MembershipType\"},{\"name\":\"member_id\",\"type\":\"uuid.UUID\"},{\"name\":\"resource_type\",\"type\":\"authz.ResourceType\"},{\"name\":\"resource_id\",\"type\":\"uuid.UUID\"},{\"name\":\"parent_id\",\"type\":\"uuid.UUID\"}]},{\"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\"},{\"name\":\"context\",\"type\":\"biz.OrgInvitationContext\"}]},{\"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\":\"updated_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\"},{\"name\":\"latest\",\"type\":\"bool\"}]},{\"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\"},{\"name\":\"first_name\",\"type\":\"string\"},{\"name\":\"last_name\",\"type\":\"string\"}]},{\"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\"},{\"name\":\"scoped_resource_type\",\"type\":\"biz.ContractScope\"},{\"name\":\"scoped_resource_id\",\"type\":\"uuid.UUID\"}]},{\"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\":\"APIToken\",\"to\":\"Project\",\"label\":\"project\"},{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"CASMapping\",\"to\":\"Project\",\"label\":\"project\"},{\"from\":\"Group\",\"to\":\"User\",\"label\":\"members\"},{\"from\":\"GroupMembership\",\"to\":\"Group\",\"label\":\"group\"},{\"from\":\"GroupMembership\",\"to\":\"User\",\"label\":\"user\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"Membership\",\"to\":\"Membership\",\"label\":\"children\"},{\"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\":\"Organization\",\"to\":\"Group\",\"label\":\"groups\"},{\"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/membership.go b/app/controlplane/pkg/data/ent/schema/membership.go index 76e26a96b..95072a924 100644 --- a/app/controlplane/pkg/data/ent/schema/membership.go +++ b/app/controlplane/pkg/data/ent/schema/membership.go @@ -56,21 +56,33 @@ func (Membership) Fields() []ent.Field { field.Enum("resource_type").GoType(authz.ResourceType("")).Optional(), field.UUID("resource_id", uuid.UUID{}).Optional(), + + // Optional role inheritance + // foreign key points to the parent membership ID + field.UUID("parent_id", uuid.UUID{}).Optional().Nillable(), } } func (Membership) Edges() []ent.Edge { return []ent.Edge{ - // Deprecated: use polymorphic membership instead edge.From("organization", Organization.Type).Ref("memberships").Unique(), - // Deprecated: use polymorphic membership instead edge.From("user", User.Type).Ref("memberships").Unique(), + + // inheritance + edge.To("children", Membership.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}).From("parent").Field("parent_id").Unique(), } } func (Membership) Indexes() []ent.Index { return []ent.Index{ index.Edges("organization", "user"), - index.Fields("membership_type", "member_id", "resource_type", "resource_id").Unique(), + // only one inherited role + index.Fields("membership_type", "member_id", "resource_type", "resource_id", "role", "parent_id").Unique().Annotations( + entsql.IndexWhere("parent_id IS NOT NULL"), + ), + // only one explicit role + index.Fields("membership_type", "member_id", "resource_type", "resource_id", "role").Unique().Annotations( + entsql.IndexWhere("parent_id IS NULL"), + ), } } diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index 61308a9c4..e658d5d77 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -480,13 +480,14 @@ func (r *MembershipRepo) ListAllByResource(ctx context.Context, rt authz.Resourc return entMembershipsToBiz(mm), nil } -func (r *MembershipRepo) AddResourceRole(ctx context.Context, orgID uuid.UUID, resourceType authz.ResourceType, resID uuid.UUID, mType authz.MembershipType, memberID uuid.UUID, role authz.Role) error { +func (r *MembershipRepo) AddResourceRole(ctx context.Context, orgID uuid.UUID, resourceType authz.ResourceType, resID uuid.UUID, mType authz.MembershipType, memberID uuid.UUID, role authz.Role, parentID *uuid.UUID) error { err := r.data.DB.Membership.Create(). SetOrganizationID(orgID). SetMembershipType(mType). SetMemberID(memberID). SetResourceType(resourceType). SetResourceID(resID). + SetNillableParentID(parentID). SetRole(role).Exec(ctx) if err != nil { @@ -555,6 +556,7 @@ func entMembershipToBiz(m *ent.Membership) *biz.Membership { res.MemberID = m.MemberID res.ResourceType = m.ResourceType res.ResourceID = m.ResourceID + res.ParentID = m.ParentID return res }