diff --git a/db/migrations/00038_extensions_resources_admin_groups.sql b/db/migrations/00038_extensions_resources_admin_groups.sql new file mode 100644 index 0000000..b25d87a --- /dev/null +++ b/db/migrations/00038_extensions_resources_admin_groups.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE extension_resource_definitions ADD COLUMN IF NOT EXISTS admin_group UUID NULL REFERENCES groups(id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE extension_resource_definitions DROP COLUMN IF EXISTS admin_group; +-- +goose StatementEnd diff --git a/internal/models/extension_resource_definitions.go b/internal/models/extension_resource_definitions.go index 4a4a234..a8d44fd 100644 --- a/internal/models/extension_resource_definitions.go +++ b/internal/models/extension_resource_definitions.go @@ -25,19 +25,20 @@ import ( // ExtensionResourceDefinition is an object representing the database table. type ExtensionResourceDefinition struct { - ID string `boil:"id" json:"id" toml:"id" yaml:"id"` - Name string `boil:"name" json:"name" toml:"name" yaml:"name"` - Description string `boil:"description" json:"description" toml:"description" yaml:"description"` - Enabled bool `boil:"enabled" json:"enabled" toml:"enabled" yaml:"enabled"` - SlugSingular string `boil:"slug_singular" json:"slug_singular" toml:"slug_singular" yaml:"slug_singular"` - SlugPlural string `boil:"slug_plural" json:"slug_plural" toml:"slug_plural" yaml:"slug_plural"` - Version string `boil:"version" json:"version" toml:"version" yaml:"version"` - Scope string `boil:"scope" json:"scope" toml:"scope" yaml:"scope"` - Schema types.JSON `boil:"schema" json:"schema" toml:"schema" yaml:"schema"` - CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` - UpdatedAt time.Time `boil:"updated_at" json:"updated_at" toml:"updated_at" yaml:"updated_at"` - DeletedAt null.Time `boil:"deleted_at" json:"deleted_at,omitempty" toml:"deleted_at" yaml:"deleted_at,omitempty"` - ExtensionID string `boil:"extension_id" json:"extension_id" toml:"extension_id" yaml:"extension_id"` + ID string `boil:"id" json:"id" toml:"id" yaml:"id"` + Name string `boil:"name" json:"name" toml:"name" yaml:"name"` + Description string `boil:"description" json:"description" toml:"description" yaml:"description"` + Enabled bool `boil:"enabled" json:"enabled" toml:"enabled" yaml:"enabled"` + SlugSingular string `boil:"slug_singular" json:"slug_singular" toml:"slug_singular" yaml:"slug_singular"` + SlugPlural string `boil:"slug_plural" json:"slug_plural" toml:"slug_plural" yaml:"slug_plural"` + Version string `boil:"version" json:"version" toml:"version" yaml:"version"` + Scope string `boil:"scope" json:"scope" toml:"scope" yaml:"scope"` + Schema types.JSON `boil:"schema" json:"schema" toml:"schema" yaml:"schema"` + CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` + UpdatedAt time.Time `boil:"updated_at" json:"updated_at" toml:"updated_at" yaml:"updated_at"` + DeletedAt null.Time `boil:"deleted_at" json:"deleted_at,omitempty" toml:"deleted_at" yaml:"deleted_at,omitempty"` + ExtensionID string `boil:"extension_id" json:"extension_id" toml:"extension_id" yaml:"extension_id"` + AdminGroup null.String `boil:"admin_group" json:"admin_group,omitempty" toml:"admin_group" yaml:"admin_group,omitempty"` R *extensionResourceDefinitionR `boil:"-" json:"-" toml:"-" yaml:"-"` L extensionResourceDefinitionL `boil:"-" json:"-" toml:"-" yaml:"-"` @@ -57,6 +58,7 @@ var ExtensionResourceDefinitionColumns = struct { UpdatedAt string DeletedAt string ExtensionID string + AdminGroup string }{ ID: "id", Name: "name", @@ -71,6 +73,7 @@ var ExtensionResourceDefinitionColumns = struct { UpdatedAt: "updated_at", DeletedAt: "deleted_at", ExtensionID: "extension_id", + AdminGroup: "admin_group", } var ExtensionResourceDefinitionTableColumns = struct { @@ -87,6 +90,7 @@ var ExtensionResourceDefinitionTableColumns = struct { UpdatedAt string DeletedAt string ExtensionID string + AdminGroup string }{ ID: "extension_resource_definitions.id", Name: "extension_resource_definitions.name", @@ -101,6 +105,7 @@ var ExtensionResourceDefinitionTableColumns = struct { UpdatedAt: "extension_resource_definitions.updated_at", DeletedAt: "extension_resource_definitions.deleted_at", ExtensionID: "extension_resource_definitions.extension_id", + AdminGroup: "extension_resource_definitions.admin_group", } // Generated where @@ -149,6 +154,7 @@ var ExtensionResourceDefinitionWhere = struct { UpdatedAt whereHelpertime_Time DeletedAt whereHelpernull_Time ExtensionID whereHelperstring + AdminGroup whereHelpernull_String }{ ID: whereHelperstring{field: "\"extension_resource_definitions\".\"id\""}, Name: whereHelperstring{field: "\"extension_resource_definitions\".\"name\""}, @@ -163,15 +169,18 @@ var ExtensionResourceDefinitionWhere = struct { UpdatedAt: whereHelpertime_Time{field: "\"extension_resource_definitions\".\"updated_at\""}, DeletedAt: whereHelpernull_Time{field: "\"extension_resource_definitions\".\"deleted_at\""}, ExtensionID: whereHelperstring{field: "\"extension_resource_definitions\".\"extension_id\""}, + AdminGroup: whereHelpernull_String{field: "\"extension_resource_definitions\".\"admin_group\""}, } // ExtensionResourceDefinitionRels is where relationship names are stored. var ExtensionResourceDefinitionRels = struct { Extension string + AdminGroupGroup string SystemExtensionResources string UserExtensionResources string }{ Extension: "Extension", + AdminGroupGroup: "AdminGroupGroup", SystemExtensionResources: "SystemExtensionResources", UserExtensionResources: "UserExtensionResources", } @@ -179,6 +188,7 @@ var ExtensionResourceDefinitionRels = struct { // extensionResourceDefinitionR is where relationships are stored. type extensionResourceDefinitionR struct { Extension *Extension `boil:"Extension" json:"Extension" toml:"Extension" yaml:"Extension"` + AdminGroupGroup *Group `boil:"AdminGroupGroup" json:"AdminGroupGroup" toml:"AdminGroupGroup" yaml:"AdminGroupGroup"` SystemExtensionResources SystemExtensionResourceSlice `boil:"SystemExtensionResources" json:"SystemExtensionResources" toml:"SystemExtensionResources" yaml:"SystemExtensionResources"` UserExtensionResources UserExtensionResourceSlice `boil:"UserExtensionResources" json:"UserExtensionResources" toml:"UserExtensionResources" yaml:"UserExtensionResources"` } @@ -195,6 +205,13 @@ func (r *extensionResourceDefinitionR) GetExtension() *Extension { return r.Extension } +func (r *extensionResourceDefinitionR) GetAdminGroupGroup() *Group { + if r == nil { + return nil + } + return r.AdminGroupGroup +} + func (r *extensionResourceDefinitionR) GetSystemExtensionResources() SystemExtensionResourceSlice { if r == nil { return nil @@ -213,9 +230,9 @@ func (r *extensionResourceDefinitionR) GetUserExtensionResources() UserExtension type extensionResourceDefinitionL struct{} var ( - extensionResourceDefinitionAllColumns = []string{"id", "name", "description", "enabled", "slug_singular", "slug_plural", "version", "scope", "schema", "created_at", "updated_at", "deleted_at", "extension_id"} + extensionResourceDefinitionAllColumns = []string{"id", "name", "description", "enabled", "slug_singular", "slug_plural", "version", "scope", "schema", "created_at", "updated_at", "deleted_at", "extension_id", "admin_group"} extensionResourceDefinitionColumnsWithoutDefault = []string{"name", "description", "slug_singular", "slug_plural", "version", "scope", "schema", "extension_id"} - extensionResourceDefinitionColumnsWithDefault = []string{"id", "enabled", "created_at", "updated_at", "deleted_at"} + extensionResourceDefinitionColumnsWithDefault = []string{"id", "enabled", "created_at", "updated_at", "deleted_at", "admin_group"} extensionResourceDefinitionPrimaryKeyColumns = []string{"id"} extensionResourceDefinitionGeneratedColumns = []string{} ) @@ -536,6 +553,17 @@ func (o *ExtensionResourceDefinition) Extension(mods ...qm.QueryMod) extensionQu return Extensions(queryMods...) } +// AdminGroupGroup pointed to by the foreign key. +func (o *ExtensionResourceDefinition) AdminGroupGroup(mods ...qm.QueryMod) groupQuery { + queryMods := []qm.QueryMod{ + qm.Where("\"id\" = ?", o.AdminGroup), + } + + queryMods = append(queryMods, mods...) + + return Groups(queryMods...) +} + // SystemExtensionResources retrieves all the system_extension_resource's SystemExtensionResources with an executor. func (o *ExtensionResourceDefinition) SystemExtensionResources(mods ...qm.QueryMod) systemExtensionResourceQuery { var queryMods []qm.QueryMod @@ -685,6 +713,131 @@ func (extensionResourceDefinitionL) LoadExtension(ctx context.Context, e boil.Co return nil } +// LoadAdminGroupGroup allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for an N-1 relationship. +func (extensionResourceDefinitionL) LoadAdminGroupGroup(ctx context.Context, e boil.ContextExecutor, singular bool, maybeExtensionResourceDefinition interface{}, mods queries.Applicator) error { + var slice []*ExtensionResourceDefinition + var object *ExtensionResourceDefinition + + if singular { + var ok bool + object, ok = maybeExtensionResourceDefinition.(*ExtensionResourceDefinition) + if !ok { + object = new(ExtensionResourceDefinition) + ok = queries.SetFromEmbeddedStruct(&object, &maybeExtensionResourceDefinition) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", object, maybeExtensionResourceDefinition)) + } + } + } else { + s, ok := maybeExtensionResourceDefinition.(*[]*ExtensionResourceDefinition) + if ok { + slice = *s + } else { + ok = queries.SetFromEmbeddedStruct(&slice, maybeExtensionResourceDefinition) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", slice, maybeExtensionResourceDefinition)) + } + } + } + + args := make(map[interface{}]struct{}) + if singular { + if object.R == nil { + object.R = &extensionResourceDefinitionR{} + } + if !queries.IsNil(object.AdminGroup) { + args[object.AdminGroup] = struct{}{} + } + + } else { + for _, obj := range slice { + if obj.R == nil { + obj.R = &extensionResourceDefinitionR{} + } + + if !queries.IsNil(obj.AdminGroup) { + args[obj.AdminGroup] = struct{}{} + } + + } + } + + if len(args) == 0 { + return nil + } + + argsSlice := make([]interface{}, len(args)) + i := 0 + for arg := range args { + argsSlice[i] = arg + i++ + } + + query := NewQuery( + qm.From(`groups`), + qm.WhereIn(`groups.id in ?`, argsSlice...), + qmhelper.WhereIsNull(`groups.deleted_at`), + ) + if mods != nil { + mods.Apply(query) + } + + results, err := query.QueryContext(ctx, e) + if err != nil { + return errors.Wrap(err, "failed to eager load Group") + } + + var resultSlice []*Group + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice Group") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results of eager load for groups") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for groups") + } + + if len(groupAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + + if len(resultSlice) == 0 { + return nil + } + + if singular { + foreign := resultSlice[0] + object.R.AdminGroupGroup = foreign + if foreign.R == nil { + foreign.R = &groupR{} + } + foreign.R.AdminGroupExtensionResourceDefinitions = append(foreign.R.AdminGroupExtensionResourceDefinitions, object) + return nil + } + + for _, local := range slice { + for _, foreign := range resultSlice { + if queries.Equal(local.AdminGroup, foreign.ID) { + local.R.AdminGroupGroup = foreign + if foreign.R == nil { + foreign.R = &groupR{} + } + foreign.R.AdminGroupExtensionResourceDefinitions = append(foreign.R.AdminGroupExtensionResourceDefinitions, local) + break + } + } + } + + return nil +} + // LoadSystemExtensionResources allows an eager lookup of values, cached into the // loaded structs of the objects. This is for a 1-M or N-M relationship. func (extensionResourceDefinitionL) LoadSystemExtensionResources(ctx context.Context, e boil.ContextExecutor, singular bool, maybeExtensionResourceDefinition interface{}, mods queries.Applicator) error { @@ -960,6 +1113,86 @@ func (o *ExtensionResourceDefinition) SetExtension(ctx context.Context, exec boi return nil } +// SetAdminGroupGroup of the extensionResourceDefinition to the related item. +// Sets o.R.AdminGroupGroup to related. +// Adds o to related.R.AdminGroupExtensionResourceDefinitions. +func (o *ExtensionResourceDefinition) SetAdminGroupGroup(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Group) error { + var err error + if insert { + if err = related.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } + + updateQuery := fmt.Sprintf( + "UPDATE \"extension_resource_definitions\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, []string{"admin_group"}), + strmangle.WhereClause("\"", "\"", 2, extensionResourceDefinitionPrimaryKeyColumns), + ) + values := []interface{}{related.ID, o.ID} + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, updateQuery) + fmt.Fprintln(writer, values) + } + if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil { + return errors.Wrap(err, "failed to update local table") + } + + queries.Assign(&o.AdminGroup, related.ID) + if o.R == nil { + o.R = &extensionResourceDefinitionR{ + AdminGroupGroup: related, + } + } else { + o.R.AdminGroupGroup = related + } + + if related.R == nil { + related.R = &groupR{ + AdminGroupExtensionResourceDefinitions: ExtensionResourceDefinitionSlice{o}, + } + } else { + related.R.AdminGroupExtensionResourceDefinitions = append(related.R.AdminGroupExtensionResourceDefinitions, o) + } + + return nil +} + +// RemoveAdminGroupGroup relationship. +// Sets o.R.AdminGroupGroup to nil. +// Removes o from all passed in related items' relationships struct. +func (o *ExtensionResourceDefinition) RemoveAdminGroupGroup(ctx context.Context, exec boil.ContextExecutor, related *Group) error { + var err error + + queries.SetScanner(&o.AdminGroup, nil) + if _, err = o.Update(ctx, exec, boil.Whitelist("admin_group")); err != nil { + return errors.Wrap(err, "failed to update local table") + } + + if o.R != nil { + o.R.AdminGroupGroup = nil + } + if related == nil || related.R == nil { + return nil + } + + for i, ri := range related.R.AdminGroupExtensionResourceDefinitions { + if queries.Equal(o.AdminGroup, ri.AdminGroup) { + continue + } + + ln := len(related.R.AdminGroupExtensionResourceDefinitions) + if ln > 1 && i < ln-1 { + related.R.AdminGroupExtensionResourceDefinitions[i] = related.R.AdminGroupExtensionResourceDefinitions[ln-1] + } + related.R.AdminGroupExtensionResourceDefinitions = related.R.AdminGroupExtensionResourceDefinitions[:ln-1] + break + } + return nil +} + // AddSystemExtensionResources adds the given related objects to the existing relationships // of the extension_resource_definition, optionally inserting them as new records. // Appends related to o.R.SystemExtensionResources. diff --git a/internal/models/groups.go b/internal/models/groups.go index 87b9939..9777068 100644 --- a/internal/models/groups.go +++ b/internal/models/groups.go @@ -108,47 +108,50 @@ var GroupWhere = struct { // GroupRels is where relationship names are stored. var GroupRels = struct { - ApproverGroupGroup string - ApproverGroupApplications string - SubjectGroupAuditEvents string - GroupApplicationRequests string - ApproverGroupGroupApplicationRequests string - GroupApplications string - ParentGroupGroupHierarchies string - MemberGroupGroupHierarchies string - GroupMembershipRequests string - GroupMemberships string - GroupOrganizations string - ApproverGroupGroups string + ApproverGroupGroup string + ApproverGroupApplications string + SubjectGroupAuditEvents string + AdminGroupExtensionResourceDefinitions string + GroupApplicationRequests string + ApproverGroupGroupApplicationRequests string + GroupApplications string + ParentGroupGroupHierarchies string + MemberGroupGroupHierarchies string + GroupMembershipRequests string + GroupMemberships string + GroupOrganizations string + ApproverGroupGroups string }{ - ApproverGroupGroup: "ApproverGroupGroup", - ApproverGroupApplications: "ApproverGroupApplications", - SubjectGroupAuditEvents: "SubjectGroupAuditEvents", - GroupApplicationRequests: "GroupApplicationRequests", - ApproverGroupGroupApplicationRequests: "ApproverGroupGroupApplicationRequests", - GroupApplications: "GroupApplications", - ParentGroupGroupHierarchies: "ParentGroupGroupHierarchies", - MemberGroupGroupHierarchies: "MemberGroupGroupHierarchies", - GroupMembershipRequests: "GroupMembershipRequests", - GroupMemberships: "GroupMemberships", - GroupOrganizations: "GroupOrganizations", - ApproverGroupGroups: "ApproverGroupGroups", + ApproverGroupGroup: "ApproverGroupGroup", + ApproverGroupApplications: "ApproverGroupApplications", + SubjectGroupAuditEvents: "SubjectGroupAuditEvents", + AdminGroupExtensionResourceDefinitions: "AdminGroupExtensionResourceDefinitions", + GroupApplicationRequests: "GroupApplicationRequests", + ApproverGroupGroupApplicationRequests: "ApproverGroupGroupApplicationRequests", + GroupApplications: "GroupApplications", + ParentGroupGroupHierarchies: "ParentGroupGroupHierarchies", + MemberGroupGroupHierarchies: "MemberGroupGroupHierarchies", + GroupMembershipRequests: "GroupMembershipRequests", + GroupMemberships: "GroupMemberships", + GroupOrganizations: "GroupOrganizations", + ApproverGroupGroups: "ApproverGroupGroups", } // groupR is where relationships are stored. type groupR struct { - ApproverGroupGroup *Group `boil:"ApproverGroupGroup" json:"ApproverGroupGroup" toml:"ApproverGroupGroup" yaml:"ApproverGroupGroup"` - ApproverGroupApplications ApplicationSlice `boil:"ApproverGroupApplications" json:"ApproverGroupApplications" toml:"ApproverGroupApplications" yaml:"ApproverGroupApplications"` - SubjectGroupAuditEvents AuditEventSlice `boil:"SubjectGroupAuditEvents" json:"SubjectGroupAuditEvents" toml:"SubjectGroupAuditEvents" yaml:"SubjectGroupAuditEvents"` - GroupApplicationRequests GroupApplicationRequestSlice `boil:"GroupApplicationRequests" json:"GroupApplicationRequests" toml:"GroupApplicationRequests" yaml:"GroupApplicationRequests"` - ApproverGroupGroupApplicationRequests GroupApplicationRequestSlice `boil:"ApproverGroupGroupApplicationRequests" json:"ApproverGroupGroupApplicationRequests" toml:"ApproverGroupGroupApplicationRequests" yaml:"ApproverGroupGroupApplicationRequests"` - GroupApplications GroupApplicationSlice `boil:"GroupApplications" json:"GroupApplications" toml:"GroupApplications" yaml:"GroupApplications"` - ParentGroupGroupHierarchies GroupHierarchySlice `boil:"ParentGroupGroupHierarchies" json:"ParentGroupGroupHierarchies" toml:"ParentGroupGroupHierarchies" yaml:"ParentGroupGroupHierarchies"` - MemberGroupGroupHierarchies GroupHierarchySlice `boil:"MemberGroupGroupHierarchies" json:"MemberGroupGroupHierarchies" toml:"MemberGroupGroupHierarchies" yaml:"MemberGroupGroupHierarchies"` - GroupMembershipRequests GroupMembershipRequestSlice `boil:"GroupMembershipRequests" json:"GroupMembershipRequests" toml:"GroupMembershipRequests" yaml:"GroupMembershipRequests"` - GroupMemberships GroupMembershipSlice `boil:"GroupMemberships" json:"GroupMemberships" toml:"GroupMemberships" yaml:"GroupMemberships"` - GroupOrganizations GroupOrganizationSlice `boil:"GroupOrganizations" json:"GroupOrganizations" toml:"GroupOrganizations" yaml:"GroupOrganizations"` - ApproverGroupGroups GroupSlice `boil:"ApproverGroupGroups" json:"ApproverGroupGroups" toml:"ApproverGroupGroups" yaml:"ApproverGroupGroups"` + ApproverGroupGroup *Group `boil:"ApproverGroupGroup" json:"ApproverGroupGroup" toml:"ApproverGroupGroup" yaml:"ApproverGroupGroup"` + ApproverGroupApplications ApplicationSlice `boil:"ApproverGroupApplications" json:"ApproverGroupApplications" toml:"ApproverGroupApplications" yaml:"ApproverGroupApplications"` + SubjectGroupAuditEvents AuditEventSlice `boil:"SubjectGroupAuditEvents" json:"SubjectGroupAuditEvents" toml:"SubjectGroupAuditEvents" yaml:"SubjectGroupAuditEvents"` + AdminGroupExtensionResourceDefinitions ExtensionResourceDefinitionSlice `boil:"AdminGroupExtensionResourceDefinitions" json:"AdminGroupExtensionResourceDefinitions" toml:"AdminGroupExtensionResourceDefinitions" yaml:"AdminGroupExtensionResourceDefinitions"` + GroupApplicationRequests GroupApplicationRequestSlice `boil:"GroupApplicationRequests" json:"GroupApplicationRequests" toml:"GroupApplicationRequests" yaml:"GroupApplicationRequests"` + ApproverGroupGroupApplicationRequests GroupApplicationRequestSlice `boil:"ApproverGroupGroupApplicationRequests" json:"ApproverGroupGroupApplicationRequests" toml:"ApproverGroupGroupApplicationRequests" yaml:"ApproverGroupGroupApplicationRequests"` + GroupApplications GroupApplicationSlice `boil:"GroupApplications" json:"GroupApplications" toml:"GroupApplications" yaml:"GroupApplications"` + ParentGroupGroupHierarchies GroupHierarchySlice `boil:"ParentGroupGroupHierarchies" json:"ParentGroupGroupHierarchies" toml:"ParentGroupGroupHierarchies" yaml:"ParentGroupGroupHierarchies"` + MemberGroupGroupHierarchies GroupHierarchySlice `boil:"MemberGroupGroupHierarchies" json:"MemberGroupGroupHierarchies" toml:"MemberGroupGroupHierarchies" yaml:"MemberGroupGroupHierarchies"` + GroupMembershipRequests GroupMembershipRequestSlice `boil:"GroupMembershipRequests" json:"GroupMembershipRequests" toml:"GroupMembershipRequests" yaml:"GroupMembershipRequests"` + GroupMemberships GroupMembershipSlice `boil:"GroupMemberships" json:"GroupMemberships" toml:"GroupMemberships" yaml:"GroupMemberships"` + GroupOrganizations GroupOrganizationSlice `boil:"GroupOrganizations" json:"GroupOrganizations" toml:"GroupOrganizations" yaml:"GroupOrganizations"` + ApproverGroupGroups GroupSlice `boil:"ApproverGroupGroups" json:"ApproverGroupGroups" toml:"ApproverGroupGroups" yaml:"ApproverGroupGroups"` } // NewStruct creates a new relationship struct @@ -177,6 +180,13 @@ func (r *groupR) GetSubjectGroupAuditEvents() AuditEventSlice { return r.SubjectGroupAuditEvents } +func (r *groupR) GetAdminGroupExtensionResourceDefinitions() ExtensionResourceDefinitionSlice { + if r == nil { + return nil + } + return r.AdminGroupExtensionResourceDefinitions +} + func (r *groupR) GetGroupApplicationRequests() GroupApplicationRequestSlice { if r == nil { return nil @@ -595,6 +605,20 @@ func (o *Group) SubjectGroupAuditEvents(mods ...qm.QueryMod) auditEventQuery { return AuditEvents(queryMods...) } +// AdminGroupExtensionResourceDefinitions retrieves all the extension_resource_definition's ExtensionResourceDefinitions with an executor via admin_group column. +func (o *Group) AdminGroupExtensionResourceDefinitions(mods ...qm.QueryMod) extensionResourceDefinitionQuery { + var queryMods []qm.QueryMod + if len(mods) != 0 { + queryMods = append(queryMods, mods...) + } + + queryMods = append(queryMods, + qm.Where("\"extension_resource_definitions\".\"admin_group\"=?", o.ID), + ) + + return ExtensionResourceDefinitions(queryMods...) +} + // GroupApplicationRequests retrieves all the group_application_request's GroupApplicationRequests with an executor. func (o *Group) GroupApplicationRequests(mods ...qm.QueryMod) groupApplicationRequestQuery { var queryMods []qm.QueryMod @@ -1073,6 +1097,120 @@ func (groupL) LoadSubjectGroupAuditEvents(ctx context.Context, e boil.ContextExe return nil } +// LoadAdminGroupExtensionResourceDefinitions allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for a 1-M or N-M relationship. +func (groupL) LoadAdminGroupExtensionResourceDefinitions(ctx context.Context, e boil.ContextExecutor, singular bool, maybeGroup interface{}, mods queries.Applicator) error { + var slice []*Group + var object *Group + + if singular { + var ok bool + object, ok = maybeGroup.(*Group) + if !ok { + object = new(Group) + ok = queries.SetFromEmbeddedStruct(&object, &maybeGroup) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", object, maybeGroup)) + } + } + } else { + s, ok := maybeGroup.(*[]*Group) + if ok { + slice = *s + } else { + ok = queries.SetFromEmbeddedStruct(&slice, maybeGroup) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", slice, maybeGroup)) + } + } + } + + args := make(map[interface{}]struct{}) + if singular { + if object.R == nil { + object.R = &groupR{} + } + args[object.ID] = struct{}{} + } else { + for _, obj := range slice { + if obj.R == nil { + obj.R = &groupR{} + } + args[obj.ID] = struct{}{} + } + } + + if len(args) == 0 { + return nil + } + + argsSlice := make([]interface{}, len(args)) + i := 0 + for arg := range args { + argsSlice[i] = arg + i++ + } + + query := NewQuery( + qm.From(`extension_resource_definitions`), + qm.WhereIn(`extension_resource_definitions.admin_group in ?`, argsSlice...), + qmhelper.WhereIsNull(`extension_resource_definitions.deleted_at`), + ) + if mods != nil { + mods.Apply(query) + } + + results, err := query.QueryContext(ctx, e) + if err != nil { + return errors.Wrap(err, "failed to eager load extension_resource_definitions") + } + + var resultSlice []*ExtensionResourceDefinition + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice extension_resource_definitions") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results in eager load on extension_resource_definitions") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for extension_resource_definitions") + } + + if len(extensionResourceDefinitionAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + if singular { + object.R.AdminGroupExtensionResourceDefinitions = resultSlice + for _, foreign := range resultSlice { + if foreign.R == nil { + foreign.R = &extensionResourceDefinitionR{} + } + foreign.R.AdminGroupGroup = object + } + return nil + } + + for _, foreign := range resultSlice { + for _, local := range slice { + if queries.Equal(local.ID, foreign.AdminGroup) { + local.R.AdminGroupExtensionResourceDefinitions = append(local.R.AdminGroupExtensionResourceDefinitions, foreign) + if foreign.R == nil { + foreign.R = &extensionResourceDefinitionR{} + } + foreign.R.AdminGroupGroup = local + break + } + } + } + + return nil +} + // LoadGroupApplicationRequests allows an eager lookup of values, cached into the // loaded structs of the objects. This is for a 1-M or N-M relationship. func (groupL) LoadGroupApplicationRequests(ctx context.Context, e boil.ContextExecutor, singular bool, maybeGroup interface{}, mods queries.Applicator) error { @@ -2426,6 +2564,133 @@ func (o *Group) RemoveSubjectGroupAuditEvents(ctx context.Context, exec boil.Con return nil } +// AddAdminGroupExtensionResourceDefinitions adds the given related objects to the existing relationships +// of the group, optionally inserting them as new records. +// Appends related to o.R.AdminGroupExtensionResourceDefinitions. +// Sets related.R.AdminGroupGroup appropriately. +func (o *Group) AddAdminGroupExtensionResourceDefinitions(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*ExtensionResourceDefinition) error { + var err error + for _, rel := range related { + if insert { + queries.Assign(&rel.AdminGroup, o.ID) + if err = rel.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } else { + updateQuery := fmt.Sprintf( + "UPDATE \"extension_resource_definitions\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, []string{"admin_group"}), + strmangle.WhereClause("\"", "\"", 2, extensionResourceDefinitionPrimaryKeyColumns), + ) + values := []interface{}{o.ID, rel.ID} + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, updateQuery) + fmt.Fprintln(writer, values) + } + if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil { + return errors.Wrap(err, "failed to update foreign table") + } + + queries.Assign(&rel.AdminGroup, o.ID) + } + } + + if o.R == nil { + o.R = &groupR{ + AdminGroupExtensionResourceDefinitions: related, + } + } else { + o.R.AdminGroupExtensionResourceDefinitions = append(o.R.AdminGroupExtensionResourceDefinitions, related...) + } + + for _, rel := range related { + if rel.R == nil { + rel.R = &extensionResourceDefinitionR{ + AdminGroupGroup: o, + } + } else { + rel.R.AdminGroupGroup = o + } + } + return nil +} + +// SetAdminGroupExtensionResourceDefinitions removes all previously related items of the +// group replacing them completely with the passed +// in related items, optionally inserting them as new records. +// Sets o.R.AdminGroupGroup's AdminGroupExtensionResourceDefinitions accordingly. +// Replaces o.R.AdminGroupExtensionResourceDefinitions with related. +// Sets related.R.AdminGroupGroup's AdminGroupExtensionResourceDefinitions accordingly. +func (o *Group) SetAdminGroupExtensionResourceDefinitions(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*ExtensionResourceDefinition) error { + query := "update \"extension_resource_definitions\" set \"admin_group\" = null where \"admin_group\" = $1" + values := []interface{}{o.ID} + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, query) + fmt.Fprintln(writer, values) + } + _, err := exec.ExecContext(ctx, query, values...) + if err != nil { + return errors.Wrap(err, "failed to remove relationships before set") + } + + if o.R != nil { + for _, rel := range o.R.AdminGroupExtensionResourceDefinitions { + queries.SetScanner(&rel.AdminGroup, nil) + if rel.R == nil { + continue + } + + rel.R.AdminGroupGroup = nil + } + o.R.AdminGroupExtensionResourceDefinitions = nil + } + + return o.AddAdminGroupExtensionResourceDefinitions(ctx, exec, insert, related...) +} + +// RemoveAdminGroupExtensionResourceDefinitions relationships from objects passed in. +// Removes related items from R.AdminGroupExtensionResourceDefinitions (uses pointer comparison, removal does not keep order) +// Sets related.R.AdminGroupGroup. +func (o *Group) RemoveAdminGroupExtensionResourceDefinitions(ctx context.Context, exec boil.ContextExecutor, related ...*ExtensionResourceDefinition) error { + if len(related) == 0 { + return nil + } + + var err error + for _, rel := range related { + queries.SetScanner(&rel.AdminGroup, nil) + if rel.R != nil { + rel.R.AdminGroupGroup = nil + } + if _, err = rel.Update(ctx, exec, boil.Whitelist("admin_group")); err != nil { + return err + } + } + if o.R == nil { + return nil + } + + for _, rel := range related { + for i, ri := range o.R.AdminGroupExtensionResourceDefinitions { + if rel != ri { + continue + } + + ln := len(o.R.AdminGroupExtensionResourceDefinitions) + if ln > 1 && i < ln-1 { + o.R.AdminGroupExtensionResourceDefinitions[i] = o.R.AdminGroupExtensionResourceDefinitions[ln-1] + } + o.R.AdminGroupExtensionResourceDefinitions = o.R.AdminGroupExtensionResourceDefinitions[:ln-1] + break + } + } + + return nil +} + // AddGroupApplicationRequests adds the given related objects to the existing relationships // of the group, optionally inserting them as new records. // Appends related to o.R.GroupApplicationRequests. diff --git a/pkg/api/v1alpha1/extension_lifecycle.go b/pkg/api/v1alpha1/extension_lifecycle.go index b5e5a92..795164d 100644 --- a/pkg/api/v1alpha1/extension_lifecycle.go +++ b/pkg/api/v1alpha1/extension_lifecycle.go @@ -67,24 +67,22 @@ func findERDForExtensionResource( extensionSlug, erdSlugPlural, erdVersion string, ) (extension *models.Extension, erd *models.ExtensionResourceDefinition, err error) { // fetch extension - if extension == nil { - extensionQM := qm.Where("slug = ?", extensionSlug) + extensionQM := qm.Where("slug = ?", extensionSlug) - // fetch ERD - queryMods := []qm.QueryMod{ - qm.Where("slug_plural = ?", erdSlugPlural), - qm.Where("version = ?", erdVersion), - } + // fetch ERD + queryMods := []qm.QueryMod{ + qm.Where("slug_plural = ?", erdSlugPlural), + qm.Where("version = ?", erdVersion), + } - extension, err = fetchExtension(c, exec, extensionQM, - qm.Load( - models.ExtensionRels.ExtensionResourceDefinitions, - queryMods..., - ), - ) - if err != nil { - return - } + extension, err = fetchExtension(c, exec, extensionQM, + qm.Load( + models.ExtensionRels.ExtensionResourceDefinitions, + queryMods..., + ), + ) + if err != nil { + return } if len(extension.R.ExtensionResourceDefinitions) < 1 { @@ -109,19 +107,31 @@ func (r *Router) mwExtensionResourcesEnabledCheck(c *gin.Context) { ) // find ERD - ext, erd, err := findERDForExtensionResource( - c, r.DB, - extensionSlug, erdSlugPlural, erdVersion, - ) - if err != nil { - if errors.Is(err, ErrExtensionNotFound) || errors.Is(err, ErrERDNotFound) { - sendError(c, http.StatusNotFound, err.Error()) + + ext := getCtxExtension(c) + erd := getCtxERD(c) + + // only check DB if extension or ERD is not loaded + if ext == nil || erd == nil { + var err error + + ext, erd, err = findERDForExtensionResource( + c, r.DB, + extensionSlug, erdSlugPlural, erdVersion, + ) + if err != nil { + if errors.Is(err, ErrExtensionNotFound) || errors.Is(err, ErrERDNotFound) { + sendError(c, http.StatusNotFound, err.Error()) + return + } + + sendError(c, http.StatusBadRequest, err.Error()) + return } - sendError(c, http.StatusBadRequest, err.Error()) - - return + setCtxExtension(c, ext) + setCtxERD(c, erd) } if !ext.Enabled { diff --git a/pkg/api/v1alpha1/extension_resource_auth.go b/pkg/api/v1alpha1/extension_resource_auth.go new file mode 100644 index 0000000..33439f0 --- /dev/null +++ b/pkg/api/v1alpha1/extension_resource_auth.go @@ -0,0 +1,122 @@ +package v1alpha1 + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/metal-toolbox/governor-api/internal/dbtools" + "github.com/metal-toolbox/governor-api/internal/models" + "go.uber.org/zap" +) + +const ( + contextKeyERD = "gin-contextkey/extension-resource-definition" + contextKeyExtension = "gin-contextkey/extension" +) + +func setCtxERD(c *gin.Context, u *models.ExtensionResourceDefinition) { + c.Set(contextKeyERD, u) +} + +func getCtxERD(c *gin.Context) *models.ExtensionResourceDefinition { + val, ok := c.Get(contextKeyERD) + if !ok { + return nil + } + + erd, ok := val.(*models.ExtensionResourceDefinition) + if !ok { + return nil + } + + return erd +} + +func setCtxExtension(c *gin.Context, u *models.Extension) { + c.Set(contextKeyExtension, u) +} + +func getCtxExtension(c *gin.Context) *models.Extension { + val, ok := c.Get(contextKeyExtension) + if !ok { + return nil + } + + ext, ok := val.(*models.Extension) + if !ok { + return nil + } + + return ext +} + +func (r *Router) mwSystemExtensionResourceGroupAuth(c *gin.Context) { + user := getCtxUser(c) + if user == nil { + r.Logger.Error("user not found in context") + sendError(c, http.StatusUnauthorized, "invalid user") + + return + } + + isGovAdmin := getCtxAdmin(c) + if isGovAdmin != nil && *isGovAdmin { + r.Logger.Debug("user is gov admin") + return + } + + extensionSlug := c.Param("ex-slug") + erdSlugPlural := c.Param("erd-slug-plural") + erdVersion := c.Param("erd-version") + + r.Logger.Debug( + "mwSystemExtensionResourceGroupAuth", + zap.String("extension-slug", extensionSlug), + zap.String("erd-slug-plural", erdSlugPlural), + zap.String("erd-version", erdVersion), + ) + + // find ERD + ext, erd, err := findERDForExtensionResource( + c, r.DB, + extensionSlug, erdSlugPlural, erdVersion, + ) + if err != nil { + if errors.Is(err, ErrExtensionNotFound) || errors.Is(err, ErrERDNotFound) { + sendError(c, http.StatusNotFound, err.Error()) + return + } + + sendError(c, http.StatusBadRequest, err.Error()) + + return + } + + setCtxExtension(c, ext) + setCtxERD(c, erd) + + // if user is not gov-admin and there's no admin group set for the ERD + if !erd.AdminGroup.Valid || erd.AdminGroup.String == "" { + sendError(c, http.StatusForbidden, "user do not have permissions to access this resource") + + return + } + + adminGroupID := erd.AdminGroup.String + + // check if user is part of the admin group + enumeratedMemberships, err := dbtools.GetMembershipsForUser(c, r.DB.DB, user.ID, false) + if err != nil { + sendError(c, http.StatusInternalServerError, "error getting enumerated groups: "+err.Error()) + return + } + + for _, m := range enumeratedMemberships { + if m.GroupID == adminGroupID { + return + } + } + + sendError(c, http.StatusForbidden, "user do not have permissions to access this resource") +} diff --git a/pkg/api/v1alpha1/extension_resource_auth_test.go b/pkg/api/v1alpha1/extension_resource_auth_test.go new file mode 100644 index 0000000..2342118 --- /dev/null +++ b/pkg/api/v1alpha1/extension_resource_auth_test.go @@ -0,0 +1,505 @@ +package v1alpha1 + +import ( + "bytes" + "context" + "database/sql" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/cockroachdb/cockroach-go/v2/testserver" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/metal-toolbox/auditevent/ginaudit" + dbm "github.com/metal-toolbox/governor-api/db" + "github.com/metal-toolbox/governor-api/internal/eventbus" + "github.com/metal-toolbox/governor-api/internal/models" + "github.com/pressly/goose/v3" + "github.com/stretchr/testify/suite" + "go.hollow.sh/toolbox/ginauth" + "go.uber.org/zap" +) + +type ExtensionResourcesGroupAuthTestSuite struct { + suite.Suite + + db *sql.DB + conn *mockNATSConn + + v1alpha1 *Router + + haroladAdmin *models.User + johnUser *models.User +} + +func (s *ExtensionResourcesGroupAuthTestSuite) seedTestDB() error { + testData := []string{ + // extensions + `INSERT INTO extensions (id, name, description, enabled, slug, status, created_at, updated_at) + VALUES ('00000001-0000-0000-0000-000000000001', 'Test Extension 1', 'some extension', true, 'test-extension-1', 'online', now(), now());`, + + // groups + `INSERT INTO groups (id, name, slug, description, note, created_at, updated_at) + VALUES ('00000002-0000-0000-0000-000000000001', 'Governor Admin', 'governor-admin', 'governor-admin', 'some note', now(), now());`, + `INSERT INTO groups (id, name, slug, description, note, created_at, updated_at) + VALUES ('00000002-0000-0000-0000-000000000002', 'Ext Admin', 'ext-admin', 'ext-admin', 'some note', now(), now());`, + + // test users + `INSERT INTO "users" ("id", "external_id", "name", "email", "login_count", "avatar_url", "last_login_at", "created_at", "updated_at", "github_id", "github_username", "deleted_at", "status") VALUES + ('00000003-0000-0000-0000-000000000001', NULL, 'Harold Admin', 'hadmin@email.com', 0, NULL, NULL, now(), now(), NULL, NULL, NULL, 'active');`, + `INSERT INTO "users" ("id", "external_id", "name", "email", "login_count", "avatar_url", "last_login_at", "created_at", "updated_at", "github_id", "github_username", "deleted_at", "status") VALUES + ('00000003-0000-0000-0000-000000000002', NULL, 'John User', 'juser@email.com', 0, NULL, NULL, now(), now(), NULL, NULL, NULL, 'active');`, + + // group members + // harold-admin -> governor-admin + `INSERT INTO "group_memberships" (user_id, group_id, created_at, updated_at) + VALUES ('00000003-0000-0000-0000-000000000001', '00000002-0000-0000-0000-000000000001', now(), now());`, + // john-user -> ext-admin + `INSERT INTO "group_memberships" (user_id, group_id, created_at, updated_at) + VALUES ('00000003-0000-0000-0000-000000000002', '00000002-0000-0000-0000-000000000002', now(), now());`, + + // ERDs + ` + INSERT INTO extension_resource_definitions (id, name, description, enabled, slug_singular, slug_plural, version, scope, schema, extension_id) + VALUES ('00000004-0000-0000-0000-000000000001', 'Some Resource', 'some-description', true, 'some-resource', 'some-resources', 'v1', 'system', + '{"$id": "v1.person.test-ex-1","$schema": "https://json-schema.org/draft/2020-12/schema","title": "Person","type": "object","required": ["firstName", "lastName"],"properties": {"firstName": {"type": "string","description": "The person''s first name.","ui": {"hide": true}},"lastName": {"type": "string","description": "The person''s last name."},"age": {"description": "Age in years which must be equal to or greater than zero.","type": "integer","minimum": 0}}}'::jsonb, + '00000001-0000-0000-0000-000000000001'); + `, + + // ERs + ` + INSERT INTO system_extension_resources (id, extension_resource_definition_id, resource) + VALUES ('00000005-0000-0000-0000-000000000001', '00000004-0000-0000-0000-000000000001', '{"firstName": "a", "lastName": "b"}'::jsonb); + `, + ` + INSERT INTO system_extension_resources (id, extension_resource_definition_id, resource) + VALUES ('00000005-0000-0000-0000-000000000002', '00000004-0000-0000-0000-000000000001', '{"firstName": "a", "lastName": "b"}'::jsonb); + `, + ` + INSERT INTO system_extension_resources (id, extension_resource_definition_id, resource) + VALUES ('00000005-0000-0000-0000-000000000003', '00000004-0000-0000-0000-000000000001', '{"firstName": "a", "lastName": "b"}'::jsonb); + `, + } + + for _, q := range testData { + _, err := s.db.Query(q) + if err != nil { + return err + } + } + + return nil +} + +func (s *ExtensionResourcesGroupAuthTestSuite) SetupSuite() { + gin.SetMode(gin.TestMode) + + s.conn = &mockNATSConn{} + + ts, err := testserver.NewTestServer() + if err != nil { + panic(err) + } + + s.db, err = sql.Open("postgres", ts.PGURL().String()) + if err != nil { + panic(err) + } + + goose.SetBaseFS(dbm.Migrations) + + if err := goose.Up(s.db, "migrations"); err != nil { + panic("migration failed - could not set up test db") + } + + if err := s.seedTestDB(); err != nil { + panic("db setup failed - could not seed test db: " + err.Error()) + } + + s.johnUser = &models.User{ + ID: "00000003-0000-0000-0000-000000000002", + Name: "John User", + Email: "juser@email.com", + } + + s.haroladAdmin = &models.User{ + ID: "00000003-0000-0000-0000-000000000001", + Name: "Harold Admin", + Email: "hadmin@email.com", + } + + s.v1alpha1 = &Router{ + AdminGroups: []string{"governor-admin"}, + AuthMW: &ginauth.MultiTokenMiddleware{}, + AuditMW: ginaudit.NewJSONMiddleware("governor-api", io.Discard), + DB: sqlx.NewDb(s.db, "postgres"), + EventBus: eventbus.NewClient(eventbus.WithNATSConn(s.conn)), + Logger: zap.NewNop(), + } +} + +func (s *ExtensionResourcesGroupAuthTestSuite) mwForgeUser(u *models.User, isAdmin bool) gin.HandlerFunc { + return func(c *gin.Context) { + setCtxUser(c, u) + setCtxAdmin(c, &isAdmin) + } +} + +func (s *ExtensionResourcesGroupAuthTestSuite) updateERD(ctx context.Context, payload string) error { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + auditID := uuid.New().String() + params := gin.Params{ + gin.Param{Key: "eid", Value: "00000001-0000-0000-0000-000000000001"}, + gin.Param{Key: "erd-id-slug", Value: "00000004-0000-0000-0000-000000000001"}, + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPatch, + "/api/v1alpha1/extensions/test-extension-1/erds/some-resources/v1", + io.NopCloser(bytes.NewBufferString(payload)), + ) + if err != nil { + return err + } + + c.Request = req + c.Params = params + c.Set(ginaudit.AuditIDContextKey, auditID) + + s.v1alpha1.updateExtensionResourceDefinition(c) + + if w.Code != http.StatusAccepted { + return fmt.Errorf("expected %d, got %d: resp: %s", http.StatusAccepted, w.Code, w.Body.String()) // nolint:goerr113 + } + + return nil +} + +func (s *ExtensionResourcesGroupAuthTestSuite) TestGetResources() { + tt := []struct { + name string + resourceID string + user *models.User + admin bool + respcode int + }{ + { + name: "admin-get-resources", + respcode: http.StatusOK, + resourceID: "00000005-0000-0000-0000-000000000001", + admin: true, + user: s.haroladAdmin, + }, + { + name: "non-admin-get-resources", + respcode: http.StatusOK, + resourceID: "00000005-0000-0000-0000-000000000001", + admin: false, + user: s.johnUser, + }, + } + + s.T().Parallel() + + for _, tc := range tt { + r := gin.New() + rg := r.Group("/api/v1alpha1") + rg.Use(s.mwForgeUser(tc.user, tc.admin)) + s.v1alpha1.Routes(rg) + + s.T().Run(tc.name, func(_ *testing.T) { + w := httptest.NewRecorder() + + req, err := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + fmt.Sprintf( + "/api/v1alpha1/extension-resources/test-extension-1/some-resources/v1/%s", + tc.resourceID, + ), + nil, + ) + s.Assert().NoError(err) + + r.ServeHTTP(w, req) + s.Assert().Equal(tc.respcode, w.Code, fmt.Sprintf("expected %d, got %d", tc.respcode, w.Code)) + }) + } +} + +func (s *ExtensionResourcesGroupAuthTestSuite) TestListResources() { + tt := []struct { + name string + user *models.User + admin bool + respcode int + }{ + { + name: "admin-list-resources", + respcode: http.StatusOK, + admin: true, + user: s.haroladAdmin, + }, + { + name: "non-admin-list-resources", + respcode: http.StatusOK, + admin: false, + user: s.johnUser, + }, + } + + s.T().Parallel() + + for _, tc := range tt { + r := gin.New() + rg := r.Group("/api/v1alpha1") + rg.Use(s.mwForgeUser(tc.user, tc.admin)) + s.v1alpha1.Routes(rg) + + s.T().Run(tc.name, func(_ *testing.T) { + w := httptest.NewRecorder() + + req, err := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + "/api/v1alpha1/extension-resources/test-extension-1/some-resources/v1", + nil, + ) + s.Assert().NoError(err) + + r.ServeHTTP(w, req) + s.Assert().Equal(tc.respcode, w.Code, fmt.Sprintf("expected %d, got %d", tc.respcode, w.Code)) + }) + } +} + +func (s *ExtensionResourcesGroupAuthTestSuite) TestCreateResource() { + tt := []struct { + name string + user *models.User + admin bool + respcode int + before func(context.Context) error + after func(context.Context) error + }{ + { + name: "admin-create-resource", + respcode: http.StatusCreated, + admin: true, + user: s.haroladAdmin, + }, + { + name: "non-admin-create-resource", + respcode: http.StatusForbidden, + admin: false, + user: s.johnUser, + }, + { + name: "admin-group-member-create-resource", + respcode: http.StatusCreated, + admin: false, + user: s.johnUser, + before: func(ctx context.Context) error { + return s.updateERD(ctx, `{ "admin_group": "00000002-0000-0000-0000-000000000002" }`) + }, + after: func(ctx context.Context) error { + return s.updateERD(ctx, `{ "admin_group": "" }`) + }, + }, + } + + payload := `{"firstName": "a", "lastName": "b"}` + + for _, tc := range tt { + ctx := context.Background() + + if tc.before != nil { + err := tc.before(ctx) + s.Assert().NoError(err) + } + + r := gin.New() + rg := r.Group("/api/v1alpha1") + rg.Use(s.mwForgeUser(tc.user, tc.admin)) + s.v1alpha1.Routes(rg) + + s.T().Run(tc.name, func(_ *testing.T) { + w := httptest.NewRecorder() + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + "/api/v1alpha1/extension-resources/test-extension-1/some-resources/v1", + io.NopCloser(bytes.NewBufferString(payload)), + ) + s.Assert().NoError(err) + + r.ServeHTTP(w, req) + s.Assert().Equal(tc.respcode, w.Code, fmt.Sprintf("expected %d, got %d: resp: %s", tc.respcode, w.Code, w.Body.String())) + }) + + if tc.after != nil { + err := tc.after(ctx) + s.Assert().NoError(err) + } + } +} + +func (s *ExtensionResourcesGroupAuthTestSuite) TestUpdateResource() { + tt := []struct { + name string + user *models.User + admin bool + respcode int + before func(context.Context) error + after func(context.Context) error + }{ + { + name: "admin-update-resource", + respcode: http.StatusAccepted, + admin: true, + user: s.haroladAdmin, + }, + { + name: "non-admin-update-resource", + respcode: http.StatusForbidden, + admin: false, + user: s.johnUser, + }, + { + name: "admin-group-member-update-resource", + respcode: http.StatusAccepted, + admin: false, + user: s.johnUser, + before: func(ctx context.Context) error { + return s.updateERD(ctx, `{ "admin_group": "00000002-0000-0000-0000-000000000002" }`) + }, + after: func(ctx context.Context) error { + return s.updateERD(ctx, `{ "admin_group": "" }`) + }, + }, + } + + payload := `{"firstName": "a", "lastName": "b"}` + + for _, tc := range tt { + ctx := context.Background() + + if tc.before != nil { + err := tc.before(ctx) + s.Assert().NoError(err) + } + + r := gin.New() + rg := r.Group("/api/v1alpha1") + rg.Use(s.mwForgeUser(tc.user, tc.admin)) + s.v1alpha1.Routes(rg) + + s.T().Run(tc.name, func(_ *testing.T) { + w := httptest.NewRecorder() + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPatch, + "/api/v1alpha1/extension-resources/test-extension-1/some-resources/v1/00000005-0000-0000-0000-000000000001", + io.NopCloser(bytes.NewBufferString(payload)), + ) + s.Assert().NoError(err) + + r.ServeHTTP(w, req) + s.Assert().Equal(tc.respcode, w.Code, fmt.Sprintf("expected %d, got %d: resp: %s", tc.respcode, w.Code, w.Body.String())) + }) + + if tc.after != nil { + err := tc.after(ctx) + s.Assert().NoError(err) + } + } +} + +func (s *ExtensionResourcesGroupAuthTestSuite) TestDeleteResource() { + tt := []struct { + name string + user *models.User + admin bool + respcode int + resourceID string + before func(context.Context) error + after func(context.Context) error + }{ + { + name: "admin-delete-resource", + resourceID: "00000005-0000-0000-0000-000000000002", + respcode: http.StatusAccepted, + admin: true, + user: s.haroladAdmin, + }, + { + name: "non-admin-delete-resource", + resourceID: "00000005-0000-0000-0000-000000000003", + respcode: http.StatusForbidden, + admin: false, + user: s.johnUser, + }, + { + name: "admin-group-member-delete-resource", + respcode: http.StatusAccepted, + admin: false, + user: s.johnUser, + resourceID: "00000005-0000-0000-0000-000000000003", + before: func(ctx context.Context) error { + return s.updateERD(ctx, `{ "admin_group": "00000002-0000-0000-0000-000000000002" }`) + }, + after: func(ctx context.Context) error { + return s.updateERD(ctx, `{ "admin_group": "" }`) + }, + }, + } + + for _, tc := range tt { + ctx := context.Background() + + if tc.before != nil { + err := tc.before(ctx) + s.Assert().NoError(err) + } + + r := gin.New() + rg := r.Group("/api/v1alpha1") + rg.Use(s.mwForgeUser(tc.user, tc.admin)) + s.v1alpha1.Routes(rg) + + s.T().Run(tc.name, func(_ *testing.T) { + w := httptest.NewRecorder() + + req, err := http.NewRequestWithContext( + ctx, + http.MethodDelete, + fmt.Sprintf( + "/api/v1alpha1/extension-resources/test-extension-1/some-resources/v1/%s", + tc.resourceID, + ), + nil, + ) + s.Assert().NoError(err) + + r.ServeHTTP(w, req) + s.Assert().Equal(tc.respcode, w.Code, fmt.Sprintf("expected %d, got %d: resp: %s", tc.respcode, w.Code, w.Body.String())) + }) + + if tc.after != nil { + err := tc.after(ctx) + s.Assert().NoError(err) + } + } +} + +func TestExtensionResourcesGroupAuthTestSuite(t *testing.T) { + suite.Run(t, new(ExtensionResourcesGroupAuthTestSuite)) +} diff --git a/pkg/api/v1alpha1/extension_resource_definitions.go b/pkg/api/v1alpha1/extension_resource_definitions.go index c5ea2be..46491a7 100644 --- a/pkg/api/v1alpha1/extension_resource_definitions.go +++ b/pkg/api/v1alpha1/extension_resource_definitions.go @@ -14,6 +14,7 @@ import ( "github.com/metal-toolbox/governor-api/internal/models" events "github.com/metal-toolbox/governor-api/pkg/events/v1alpha1" "github.com/metal-toolbox/governor-api/pkg/jsonschema" + "github.com/volatiletech/null/v8" "github.com/volatiletech/sqlboiler/v4/boil" "github.com/volatiletech/sqlboiler/v4/queries/qm" ) @@ -48,6 +49,7 @@ type ExtensionResourceDefinitionReq struct { Scope ExtensionResourceDefinitionScope `json:"scope"` Schema json.RawMessage `json:"schema"` Enabled *bool `json:"enabled"` + AdminGroup string `json:"admin_group"` } func isValidSlug(s string) bool { @@ -230,6 +232,7 @@ func (r *Router) createExtensionResourceDefinition(c *gin.Context) { Scope: string(req.Scope), Schema: []byte(schema), Enabled: *req.Enabled, + AdminGroup: null.NewString(req.AdminGroup, req.AdminGroup != ""), } var extensionQM qm.QueryMod @@ -540,6 +543,8 @@ func (r *Router) updateExtensionResourceDefinition(c *gin.Context) { erd.Enabled = *req.Enabled } + erd.AdminGroup = null.NewString(req.AdminGroup, req.AdminGroup != "") + tx, err := r.DB.BeginTx(c.Request.Context(), nil) if err != nil { sendError(c, http.StatusBadRequest, "error starting update transaction: "+err.Error()) diff --git a/pkg/api/v1alpha1/group_membership.go b/pkg/api/v1alpha1/group_membership.go index 813f81d..2e56d55 100644 --- a/pkg/api/v1alpha1/group_membership.go +++ b/pkg/api/v1alpha1/group_membership.go @@ -1318,10 +1318,8 @@ func (r *Router) getGroupMembershipsAll(c *gin.Context) { } else { enumeratedMemberships, err := dbtools.GetAllGroupMemberships(c, r.DB.DB, true) if err != nil { - if err != nil { - sendError(c, http.StatusInternalServerError, "error getting group memberships"+err.Error()) - return - } + sendError(c, http.StatusInternalServerError, "error getting group memberships"+err.Error()) + return } response = make([]GroupMembership, len(enumeratedMemberships)) diff --git a/pkg/api/v1alpha1/router.go b/pkg/api/v1alpha1/router.go index 29e1f58..a691aa9 100644 --- a/pkg/api/v1alpha1/router.go +++ b/pkg/api/v1alpha1/router.go @@ -675,7 +675,8 @@ func (r *Router) Routes(rg *gin.RouterGroup) { "/extension-resources/:ex-slug/:erd-slug-plural/:erd-version", r.AuditMW.AuditWithType("CreateSystemExtensionResource"), r.AuthMW.AuthRequired(createScopesWithOpenID("governor:extensionresources")), - r.mwUserAuthRequired(AuthRoleAdmin), + r.mwUserAuthRequired(AuthRoleUser), + r.mwSystemExtensionResourceGroupAuth, r.mwExtensionResourcesEnabledCheck, r.createSystemExtensionResource, ) @@ -684,6 +685,7 @@ func (r *Router) Routes(rg *gin.RouterGroup) { "/extension-resources/:ex-slug/:erd-slug-plural/:erd-version", r.AuditMW.AuditWithType("ListSystemExtensionResources"), r.AuthMW.AuthRequired(createScopesWithOpenID("governor:extensionresources")), + r.mwUserAuthRequired(AuthRoleUser), r.listSystemExtensionResources, ) @@ -691,6 +693,7 @@ func (r *Router) Routes(rg *gin.RouterGroup) { "/extension-resources/:ex-slug/:erd-slug-plural/:erd-version/:resource-id", r.AuditMW.AuditWithType("GetSystemExtensionResource"), r.AuthMW.AuthRequired(createScopesWithOpenID("governor:extensionresources")), + r.mwUserAuthRequired(AuthRoleUser), r.getSystemExtensionResource, ) @@ -698,7 +701,8 @@ func (r *Router) Routes(rg *gin.RouterGroup) { "/extension-resources/:ex-slug/:erd-slug-plural/:erd-version/:resource-id", r.AuditMW.AuditWithType("UpdateSystemExtensionResource"), r.AuthMW.AuthRequired(createScopesWithOpenID("governor:extensionresources")), - r.mwUserAuthRequired(AuthRoleAdmin), + r.mwUserAuthRequired(AuthRoleUser), + r.mwSystemExtensionResourceGroupAuth, r.mwExtensionResourcesEnabledCheck, r.updateSystemExtensionResource, ) @@ -707,7 +711,8 @@ func (r *Router) Routes(rg *gin.RouterGroup) { "/extension-resources/:ex-slug/:erd-slug-plural/:erd-version/:resource-id", r.AuditMW.AuditWithType("DeleteSystemExtensionResource"), r.AuthMW.AuthRequired(createScopesWithOpenID("governor:extensionresources")), - r.mwUserAuthRequired(AuthRoleAdmin), + r.mwUserAuthRequired(AuthRoleUser), + r.mwSystemExtensionResourceGroupAuth, r.mwExtensionResourcesEnabledCheck, r.deleteSystemExtensionResource, )