diff --git a/service/integration/actions_test.go b/service/integration/actions_test.go index 4340c84c5f..456aec3fd9 100644 --- a/service/integration/actions_test.go +++ b/service/integration/actions_test.go @@ -44,7 +44,7 @@ func (s *ActionsSuite) Test_ListActions_NoPagination_Succeeds() { fixtureCustomAction1 := s.f.GetCustomActionKey("custom_action_1") fixtureCustomAction2 := s.f.GetCustomActionKey("other_special_action") - list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{}) + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) s.NotNil(list) s.Require().NoError(err) @@ -88,7 +88,8 @@ func (s *ActionsSuite) Test_ListActions_OrdersByCreatedAt_Succeeds() { create := func(i int) string { name := fmt.Sprintf("order-test-action-%d-%d", i, suffix) created, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: name, + Name: name, + NamespaceId: s.defaultNamespaceID(), }) s.Require().NoError(err) s.Require().NotNil(created) @@ -101,7 +102,7 @@ func (s *ActionsSuite) Test_ListActions_OrdersByCreatedAt_Succeeds() { time.Sleep(5 * time.Millisecond) thirdID := create(3) - list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{}) + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) s.Require().NoError(err) s.NotNil(list) @@ -109,13 +110,14 @@ func (s *ActionsSuite) Test_ListActions_OrdersByCreatedAt_Succeeds() { } func (s *ActionsSuite) Test_ListActions_Pagination_Succeeds() { - list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{}) + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) s.NotNil(list) s.Require().NoError(err) total := list.GetPagination().GetTotal() higherOffsetThanListCount := total + 1 list, err = s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{ + NamespaceId: s.defaultNamespaceID(), Pagination: &policy.PageRequest{ Offset: higherOffsetThanListCount, }, @@ -126,6 +128,7 @@ func (s *ActionsSuite) Test_ListActions_Pagination_Succeeds() { s.Equal(higherOffsetThanListCount, list.GetPagination().GetCurrentOffset()) list, err = s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{ + NamespaceId: s.defaultNamespaceID(), Pagination: &policy.PageRequest{ Offset: 0, Limit: total - 1, @@ -140,6 +143,7 @@ func (s *ActionsSuite) Test_ListActions_Pagination_Succeeds() { func (s *ActionsSuite) Test_ListActions_LimitLargerThanConfigured_Fails() { list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{ + NamespaceId: s.defaultNamespaceID(), Pagination: &policy.PageRequest{ Limit: s.db.LimitMax + 1, }, @@ -149,6 +153,64 @@ func (s *ActionsSuite) Test_ListActions_LimitLargerThanConfigured_Fails() { s.Require().ErrorIs(err, db.ErrListLimitTooLarge) } +func (s *ActionsSuite) Test_ListActions_FiltersCustomActionsByNamespace_Succeeds() { + name := fmt.Sprintf("scoped-list-action-%d", time.Now().UnixNano()) + + first, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + + second, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.otherNamespaceID(), + }) + s.Require().NoError(err) + + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) + s.Require().NoError(err) + + foundFirst := false + foundSecond := false + for _, action := range list.GetActionsCustom() { + if action.GetId() == first.GetId() { + foundFirst = true + s.Equal(s.defaultNamespaceID(), action.GetNamespace().GetId()) + } + if action.GetId() == second.GetId() { + foundSecond = true + } + } + + s.True(foundFirst) + s.False(foundSecond) +} + +func (s *ActionsSuite) Test_ListActions_LegacyCustomAction_NamespaceProjection_Succeeds() { + legacy := s.f.GetCustomActionKey("custom_action_1") + assertLegacyProjected := func(list *actions.ListActionsResponse) { + s.T().Helper() + found := false + for _, action := range list.GetActionsCustom() { + if action.GetId() == legacy.ID { + found = true + s.Equal(s.defaultNamespaceID(), action.GetNamespace().GetId()) + s.Equal(s.defaultNamespaceFQN(), action.GetNamespace().GetFqn()) + } + } + s.True(found) + } + + listByID, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceId: s.defaultNamespaceID()}) + s.Require().NoError(err) + assertLegacyProjected(listByID) + + listByFQN, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceFqn: s.defaultNamespaceFQN()}) + s.Require().NoError(err) + assertLegacyProjected(listByFQN) +} + func (s *ActionsSuite) Test_GetAction_Id_Succeeds() { fixtureCustomAction1 := s.f.GetCustomActionKey("custom_action_1") actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) @@ -184,6 +246,7 @@ func (s *ActionsSuite) Test_GetAction_Name_Succeeds() { Identifier: &actions.GetActionRequest_Name{ Name: customAction.Name, }, + NamespaceId: s.defaultNamespaceID(), }) s.NotNil(action) s.Require().NoError(err) @@ -195,6 +258,7 @@ func (s *ActionsSuite) Test_GetAction_Name_Succeeds() { Identifier: &actions.GetActionRequest_Name{ Name: actionCreate.GetName(), }, + NamespaceId: s.defaultNamespaceID(), }) s.NotNil(action) s.Require().NoError(err) @@ -203,6 +267,97 @@ func (s *ActionsSuite) Test_GetAction_Name_Succeeds() { s.NotNil(action.GetMetadata()) } +func (s *ActionsSuite) Test_GetAction_Name_ResolvesByNamespace_Succeeds() { + name := fmt.Sprintf("scoped-get-action-%d", time.Now().UnixNano()) + + inDefaultNs, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + + inOtherNs, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.otherNamespaceID(), + }) + s.Require().NoError(err) + + gotDefault, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: name}, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + s.Equal(inDefaultNs.GetId(), gotDefault.GetId()) + s.Equal(s.defaultNamespaceID(), gotDefault.GetNamespace().GetId()) + + gotOther, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: name}, + NamespaceId: s.otherNamespaceID(), + }) + s.Require().NoError(err) + s.Equal(inOtherNs.GetId(), gotOther.GetId()) + s.Equal(s.otherNamespaceID(), gotOther.GetNamespace().GetId()) +} + +func (s *ActionsSuite) Test_GetAction_Name_LegacyCustomAction_Succeeds() { + legacy := s.f.GetCustomActionKey("other_special_action") + assertLegacyGet := func(action *policy.Action) { + s.T().Helper() + s.Equal(legacy.ID, action.GetId()) + s.Equal(s.defaultNamespaceID(), action.GetNamespace().GetId()) + s.Equal(s.defaultNamespaceFQN(), action.GetNamespace().GetFqn()) + } + + byID, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: legacy.Name}, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + assertLegacyGet(byID) + + byFQN, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: legacy.Name}, + NamespaceFqn: s.defaultNamespaceFQN(), + }) + s.Require().NoError(err) + assertLegacyGet(byFQN) +} + +func (s *ActionsSuite) Test_CreateListGetAction_WithNamespaceFQN_Succeeds() { + name := fmt.Sprintf("fqn-scoped-action-%d", time.Now().UnixNano()) + + created, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceFqn: s.otherNamespaceFQN(), + }) + s.Require().NoError(err) + s.Equal(name, created.GetName()) + s.Equal(s.otherNamespaceID(), created.GetNamespace().GetId()) + s.Equal(s.otherNamespaceFQN(), created.GetNamespace().GetFqn()) + + list, err := s.db.PolicyClient.ListActions(s.ctx, &actions.ListActionsRequest{NamespaceFqn: s.otherNamespaceFQN()}) + s.Require().NoError(err) + + found := false + for _, action := range list.GetActionsCustom() { + if action.GetId() == created.GetId() { + found = true + s.Equal(s.otherNamespaceID(), action.GetNamespace().GetId()) + s.Equal(s.otherNamespaceFQN(), action.GetNamespace().GetFqn()) + } + } + s.True(found) + + got, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Name{Name: name}, + NamespaceFqn: s.otherNamespaceFQN(), + }) + s.Require().NoError(err) + s.Equal(created.GetId(), got.GetId()) + s.Equal(s.otherNamespaceID(), got.GetNamespace().GetId()) + s.Equal(s.otherNamespaceFQN(), got.GetNamespace().GetFqn()) +} + func (s *ActionsSuite) Test_GetAction_NonExistent_Fails() { action, err := s.db.PolicyClient.GetAction(s.ctx, &actions.GetActionRequest{ Identifier: &actions.GetActionRequest_Id{ @@ -217,6 +372,7 @@ func (s *ActionsSuite) Test_GetAction_NonExistent_Fails() { Identifier: &actions.GetActionRequest_Name{ Name: "totally_unknown_action", }, + NamespaceId: s.defaultNamespaceID(), }) s.Nil(action) s.Require().Error(err) @@ -226,7 +382,8 @@ func (s *ActionsSuite) Test_GetAction_NonExistent_Fails() { func (s *ActionsSuite) Test_CreateAction_Succeeds() { newName := "new_custom_action_createaction" action, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: newName, + Name: newName, + NamespaceId: s.defaultNamespaceID(), Metadata: &common.MetadataMutable{ Labels: map[string]string{ "label1": "value1", @@ -244,11 +401,17 @@ func (s *ActionsSuite) Test_CreateAction_Succeeds() { } func (s *ActionsSuite) Test_CreateAction_Conflict_Fails() { - fixtureCustomAction := s.f.GetCustomActionKey("custom_action_1") + name := fmt.Sprintf("create-conflict-action-%d", time.Now().UnixNano()) + _, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) + s.Require().NoError(err) + action, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: fixtureCustomAction.Name, - }, - ) + Name: name, + NamespaceId: s.defaultNamespaceID(), + }) s.Nil(action) s.Require().Error(err) s.Require().ErrorIs(err, db.ErrUniqueConstraintViolation) @@ -257,7 +420,8 @@ func (s *ActionsSuite) Test_CreateAction_Conflict_Fails() { func (s *ActionsSuite) Test_CreateAction_NormalizesToLowerCase() { newName := "New_Custom_Action_CreateAction_UPPER" action, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: newName, + Name: newName, + NamespaceId: s.defaultNamespaceID(), }, ) s.NotNil(action) @@ -267,7 +431,8 @@ func (s *ActionsSuite) Test_CreateAction_NormalizesToLowerCase() { func (s *ActionsSuite) Test_UpdateAction_Succeeds() { newAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: "new_custom_action_updateaction", + Name: "new_custom_action_updateaction", + NamespaceId: s.defaultNamespaceID(), Metadata: &common.MetadataMutable{ Labels: map[string]string{ "original": "original_value", @@ -306,7 +471,8 @@ func (s *ActionsSuite) Test_UpdateAction_Succeeds() { func (s *ActionsSuite) Test_UpdateAction_NormalizesToLowerCase() { newAction, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: "testing_update_action_casing", + Name: "testing_update_action_casing", + NamespaceId: s.defaultNamespaceID(), }) s.NotNil(newAction) s.Require().NoError(err) @@ -355,7 +521,8 @@ func (s *ActionsSuite) Test_UpdateAction_NonExistent_Fails() { func (s *ActionsSuite) Test_DeleteAction_Succeeds() { created, err := s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: "new_custom_action_deleteaction", + Name: "new_custom_action_deleteaction", + NamespaceId: s.defaultNamespaceID(), }) s.NotNil(created) s.Require().NoError(err) @@ -398,6 +565,22 @@ func (s *ActionsSuite) Test_DeleteAction_StandardAction_Fails() { s.Contains(err.Error(), actionRead.GetName()) } +func (s *ActionsSuite) defaultNamespaceID() string { + return s.f.GetNamespaceKey("example.com").ID +} + +func (s *ActionsSuite) otherNamespaceID() string { + return s.f.GetNamespaceKey("example.net").ID +} + +func (s *ActionsSuite) defaultNamespaceFQN() string { + return "https://" + s.f.GetNamespaceKey("example.com").Name +} + +func (s *ActionsSuite) otherNamespaceFQN() string { + return "https://" + s.f.GetNamespaceKey("example.net").Name +} + func TestActionsSuite(t *testing.T) { if testing.Short() { t.Skip("skipping actions integration tests") diff --git a/service/integration/obligation_triggers_test.go b/service/integration/obligation_triggers_test.go index 785b1ec599..d7e57c93f7 100644 --- a/service/integration/obligation_triggers_test.go +++ b/service/integration/obligation_triggers_test.go @@ -90,7 +90,8 @@ func (s *ObligationTriggersSuite) SetupSuite() { // Create an action s.action, err = s.db.PolicyClient.CreateAction(s.ctx, &actions.CreateActionRequest{ - Name: actionName, + Name: actionName, + NamespaceId: s.namespace.GetId(), }) s.Require().NoError(err) diff --git a/service/integration/registered_resources_test.go b/service/integration/registered_resources_test.go index 2f910b9d54..1067bb6aa8 100644 --- a/service/integration/registered_resources_test.go +++ b/service/integration/registered_resources_test.go @@ -1478,7 +1478,8 @@ func (s *RegisteredResourcesSuite) Test_DeleteAction_CascadeDeleteActionAttribut // create action and resource value with action attribute values action, err := s.db.PolicyClient.CreateAction(s.ctx, &pbActions.CreateActionRequest{ - Name: "test_delete_action", + Name: "test_delete_action", + NamespaceId: s.getNamespaceID("example.com"), }) s.Require().NoError(err) @@ -1938,6 +1939,45 @@ func (s *RegisteredResourcesSuite) Test_SameNamespaceEnforcement_SameNamespace_S s.Require().Len(resVal.GetActionAttributeValues(), 1) } +func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithNamespacedCustomActionName_Succeeds() { + nsID := s.getNamespaceID("example.com") + + res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: nsID, + Name: fmt.Sprintf("test_rr_custom_action_name_%d", time.Now().UnixNano()), + }) + s.Require().NoError(err) + s.NotNil(res) + + customActionName := fmt.Sprintf("rr_custom_action_%d", time.Now().UnixNano()) + customAction, err := s.db.PolicyClient.CreateAction(s.ctx, &pbActions.CreateActionRequest{ + Name: customActionName, + NamespaceId: nsID, + }) + s.Require().NoError(err) + s.NotNil(customAction) + + resVal, err := s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: res.GetId(), + Value: fmt.Sprintf("test_rr_custom_action_name_value_%d", time.Now().UnixNano()), + ActionAttributeValues: []*registeredresources.ActionAttributeValue{ + { + ActionIdentifier: ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: customActionName, + }, + AttributeValueIdentifier: ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: "https://example.com/attr/attr1/value/value1", + }, + }, + }, + }) + s.Require().NoError(err) + s.NotNil(resVal) + s.Require().Len(resVal.GetActionAttributeValues(), 1) + s.Equal(customAction.GetId(), resVal.GetActionAttributeValues()[0].GetAction().GetId()) + s.Equal(customActionName, resVal.GetActionAttributeValues()[0].GetAction().GetName()) +} + func (s *RegisteredResourcesSuite) getNamespaceID(key string) string { ns := s.f.GetNamespaceKey(key) return ns.ID diff --git a/service/policy/db/actions.go b/service/policy/db/actions.go index ec91ef640d..adef373dfa 100644 --- a/service/policy/db/actions.go +++ b/service/policy/db/actions.go @@ -2,7 +2,6 @@ package db import ( "context" - "errors" "fmt" "strings" @@ -44,6 +43,17 @@ func (c PolicyDBClient) GetAction(ctx context.Context, req *actions.GetActionReq getActionParams.ID = pgtypeUUID(req.GetId()) case req.GetName() != "": getActionParams.Name = pgtypeText(strings.ToLower(req.GetName())) + + namespaceID := req.GetNamespaceId() + if len(namespaceID) > 0 { + parsedID := pgtypeUUID(namespaceID) + if !parsedID.Valid { + return nil, db.ErrUUIDInvalid + } + getActionParams.NamespaceID = parsedID + } else if req.GetNamespaceFqn() != "" { + getActionParams.NamespaceFqn = pgtypeText(req.GetNamespaceFqn()) + } default: return nil, db.ErrSelectIdentifierInvalid } @@ -58,10 +68,16 @@ func (c PolicyDBClient) GetAction(ctx context.Context, req *actions.GetActionReq return nil, db.WrapIfKnownInvalidQueryErr(err) } + namespace, err := hydrateNamespaceFromInterface(got.Namespace) + if err != nil { + return nil, err + } + return &policy.Action{ - Id: got.ID, - Name: got.Name, - Metadata: metadata, + Id: got.ID, + Name: got.Name, + Metadata: metadata, + Namespace: namespace, }, nil } @@ -74,8 +90,10 @@ func (c PolicyDBClient) ListActions(ctx context.Context, req *actions.ListAction } list, err := c.queries.listActions(ctx, listActionsParams{ - Limit: limit, - Offset: offset, + NamespaceID: pgtypeUUID(req.GetNamespaceId()), + NamespaceFqn: pgtypeText(req.GetNamespaceFqn()), + Limit: limit, + Offset: offset, }) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) @@ -90,10 +108,15 @@ func (c PolicyDBClient) ListActions(ctx context.Context, req *actions.ListAction if err := unmarshalMetadata(a.Metadata, metadata); err != nil { return nil, err } + namespace, err := hydrateNamespaceFromInterface(a.Namespace) + if err != nil { + return nil, err + } action := &policy.Action{ - Id: a.ID, - Name: a.Name, - Metadata: metadata, + Id: a.ID, + Name: a.Name, + Metadata: metadata, + Namespace: namespace, } if a.IsStandard { actionsStandard = append(actionsStandard, action) @@ -121,13 +144,27 @@ func (c PolicyDBClient) ListActions(ctx context.Context, req *actions.ListAction } func (c PolicyDBClient) CreateAction(ctx context.Context, req *actions.CreateActionRequest) (*policy.Action, error) { + name := strings.ToLower(req.GetName()) + if ActionStandard(name).IsValid() { + return nil, fmt.Errorf("cannot create standard action %s: %w", name, db.ErrRestrictViolation) + } + + namespaceID := req.GetNamespaceId() + useID := len(namespaceID) > 0 + parsedID := pgtypeUUID(namespaceID) + if useID && !parsedID.Valid { + return nil, db.ErrUUIDInvalid + } + metadataJSON, _, err := db.MarshalCreateMetadata(req.GetMetadata()) if err != nil { return nil, err } createParams := createCustomActionParams{ - Name: strings.ToLower(req.GetName()), - Metadata: metadataJSON, + Name: name, + Metadata: metadataJSON, + NamespaceID: parsedID, + NamespaceFqn: pgtypeText(req.GetNamespaceFqn()), } createdID, err := c.queries.createCustomAction(ctx, createParams) @@ -143,6 +180,13 @@ func (c PolicyDBClient) CreateAction(ctx context.Context, req *actions.CreateAct } func (c PolicyDBClient) UpdateAction(ctx context.Context, req *actions.UpdateActionRequest) (*policy.Action, error) { + if req.GetName() != "" { + name := strings.ToLower(req.GetName()) + if ActionStandard(name).IsValid() { + return nil, fmt.Errorf("cannot rename custom action to standard action %s: %w", name, db.ErrRestrictViolation) + } + } + // if extend we need to merge the metadata metadataJSON, metadata, err := db.MarshalUpdateMetadata(req.GetMetadata(), req.GetMetadataUpdateBehavior(), func() (*common.Metadata, error) { a, err := c.GetAction(ctx, &actions.GetActionRequest{ @@ -174,38 +218,39 @@ func (c PolicyDBClient) UpdateAction(ctx context.Context, req *actions.UpdateAct return nil, db.ErrNotFound } - return &policy.Action{ - Id: req.GetId(), - Name: req.GetName(), - Metadata: metadata, - }, nil + updated, err := c.GetAction(ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Id{ + Id: req.GetId(), + }, + }) + if err != nil { + return nil, err + } + if metadata != nil { + updated.Metadata = metadata + } + + return updated, nil } func (c PolicyDBClient) DeleteAction(ctx context.Context, req *actions.DeleteActionRequest) (*policy.Action, error) { + got, err := c.GetAction(ctx, &actions.GetActionRequest{ + Identifier: &actions.GetActionRequest_Id{ + Id: req.GetId(), + }, + }) + if err != nil { + return nil, err + } + count, err := c.queries.deleteCustomAction(ctx, req.GetId()) if err != nil { return nil, db.WrapIfKnownInvalidQueryErr(err) } - // if did not delete, was either not found or was a standard action + // if not deleted, it is a standard action because existence was verified above if count == 0 { - got, err := c.GetAction(ctx, &actions.GetActionRequest{ - Identifier: &actions.GetActionRequest_Id{ - Id: req.GetId(), - }, - }) - // not found - if err != nil && errors.Is(err, db.ErrNotFound) { - return nil, err - } - // standard action - name := strings.ToLower(got.GetName()) - if ActionStandard(name).IsValid() { - return nil, fmt.Errorf("cannot delete standard action %s: %w", name, db.ErrRestrictViolation) - } - return nil, db.ErrNotFound + return nil, fmt.Errorf("cannot delete standard action %s: %w", got.GetName(), db.ErrRestrictViolation) } - return &policy.Action{ - Id: req.GetId(), - }, nil + return got, nil } diff --git a/service/policy/db/actions.sql.go b/service/policy/db/actions.sql.go index aee20045f9..9972a65314 100644 --- a/service/policy/db/actions.sql.go +++ b/service/policy/db/actions.sql.go @@ -12,23 +12,60 @@ import ( ) const createCustomAction = `-- name: createCustomAction :one -INSERT INTO actions (name, metadata, is_standard) -VALUES ($1, $2, FALSE) +WITH ns AS ( + SELECT + $3::uuid AS id, + $4::text AS fqn +) +INSERT INTO actions (name, metadata, is_standard, namespace_id) +SELECT + $1, + $2, + FALSE, + COALESCE(ns.id, fqns.namespace_id) +FROM ns +LEFT JOIN attribute_fqns fqns ON fqns.fqn = ns.fqn AND ns.id IS NULL +WHERE + (ns.id IS NOT NULL) + OR + (ns.fqn IS NOT NULL AND fqns.namespace_id IS NOT NULL) RETURNING id ` type createCustomActionParams struct { - Name string `json:"name"` - Metadata []byte `json:"metadata"` + Name string `json:"name"` + Metadata []byte `json:"metadata"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` } // createCustomAction // -// INSERT INTO actions (name, metadata, is_standard) -// VALUES ($1, $2, FALSE) +// WITH ns AS ( +// SELECT +// $3::uuid AS id, +// $4::text AS fqn +// ) +// INSERT INTO actions (name, metadata, is_standard, namespace_id) +// SELECT +// $1, +// $2, +// FALSE, +// COALESCE(ns.id, fqns.namespace_id) +// FROM ns +// LEFT JOIN attribute_fqns fqns ON fqns.fqn = ns.fqn AND ns.id IS NULL +// WHERE +// (ns.id IS NOT NULL) +// OR +// (ns.fqn IS NOT NULL AND fqns.namespace_id IS NOT NULL) // RETURNING id func (q *Queries) createCustomAction(ctx context.Context, arg createCustomActionParams) (string, error) { - row := q.db.QueryRow(ctx, createCustomAction, arg.Name, arg.Metadata) + row := q.db.QueryRow(ctx, createCustomAction, + arg.Name, + arg.Metadata, + arg.NamespaceID, + arg.NamespaceFqn, + ) var id string err := row.Scan(&id) return id, err @@ -39,15 +76,16 @@ WITH input_actions AS ( SELECT unnest($1::text[]) AS name ), new_actions AS ( - INSERT INTO actions (name, is_standard) + INSERT INTO actions (name, is_standard, namespace_id) SELECT input.name, - FALSE -- custom actions + FALSE, -- custom actions + NULL FROM input_actions input WHERE NOT EXISTS ( - SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) + SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) AND a.namespace_id IS NULL ) - ON CONFLICT (name) DO NOTHING + ON CONFLICT (name) WHERE namespace_id IS NULL DO NOTHING RETURNING id, name, is_standard, created_at ), all_actions AS ( @@ -56,6 +94,7 @@ all_actions AS ( TRUE AS pre_existing FROM actions a JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) + WHERE a.namespace_id IS NULL UNION ALL @@ -88,15 +127,16 @@ type createOrListActionsByNameRow struct { // SELECT unnest($1::text[]) AS name // ), // new_actions AS ( -// INSERT INTO actions (name, is_standard) +// INSERT INTO actions (name, is_standard, namespace_id) // SELECT // input.name, -// FALSE -- custom actions +// FALSE, -- custom actions +// NULL // FROM input_actions input // WHERE NOT EXISTS ( -// SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) +// SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) AND a.namespace_id IS NULL // ) -// ON CONFLICT (name) DO NOTHING +// ON CONFLICT (name) WHERE namespace_id IS NULL DO NOTHING // RETURNING id, name, is_standard, created_at // ), // all_actions AS ( @@ -105,6 +145,7 @@ type createOrListActionsByNameRow struct { // TRUE AS pre_existing // FROM actions a // JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) +// WHERE a.namespace_id IS NULL // // UNION ALL // @@ -167,56 +208,177 @@ func (q *Queries) deleteCustomAction(ctx context.Context, id string) (int64, err } const getAction = `-- name: getAction :one +WITH resolved_namespace AS ( + SELECT + n.id, + n.name, + fqns.fqn + FROM attribute_namespaces n + LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL + WHERE + ($3::uuid IS NOT NULL AND n.id = $3::uuid) + OR + ($4::text IS NOT NULL AND fqns.fqn = $4::text) + LIMIT 1 +) SELECT a.id, a.name, a.is_standard, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata + JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata, + CASE + WHEN a.namespace_id IS NULL AND $1::text IS NOT NULL THEN JSON_BUILD_OBJECT( + 'id', rn.id, + 'name', rn.name, + 'fqn', rn.fqn + ) + WHEN a.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + END AS namespace FROM actions a +LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +LEFT JOIN resolved_namespace rn ON TRUE WHERE - ($1::uuid IS NULL OR a.id = $1::uuid) - AND ($2::text IS NULL OR a.name = $2::text) + ( + ($2::uuid IS NOT NULL AND a.id = $2::uuid) + OR + ( + $1::text IS NOT NULL + AND a.name = $1::text + AND ( + (rn.id IS NOT NULL AND (a.namespace_id = rn.id OR a.namespace_id IS NULL)) + OR + (rn.id IS NULL AND a.namespace_id IS NULL) + ) + ) + ) +ORDER BY + CASE + WHEN a.namespace_id = rn.id THEN 0 + WHEN a.is_standard = TRUE THEN 1 + ELSE 2 + END, + a.created_at DESC +LIMIT 1 ` type getActionParams struct { - ID pgtype.UUID `json:"id"` - Name pgtype.Text `json:"name"` + Name pgtype.Text `json:"name"` + ID pgtype.UUID `json:"id"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` } type getActionRow struct { - ID string `json:"id"` - Name string `json:"name"` - IsStandard bool `json:"is_standard"` - Metadata []byte `json:"metadata"` + ID string `json:"id"` + Name string `json:"name"` + IsStandard bool `json:"is_standard"` + Metadata []byte `json:"metadata"` + Namespace interface{} `json:"namespace"` } // getAction // +// WITH resolved_namespace AS ( +// SELECT +// n.id, +// n.name, +// fqns.fqn +// FROM attribute_namespaces n +// LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL +// WHERE +// ($3::uuid IS NOT NULL AND n.id = $3::uuid) +// OR +// ($4::text IS NOT NULL AND fqns.fqn = $4::text) +// LIMIT 1 +// ) // SELECT // a.id, // a.name, // a.is_standard, -// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata +// JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata, +// CASE +// WHEN a.namespace_id IS NULL AND $1::text IS NOT NULL THEN JSON_BUILD_OBJECT( +// 'id', rn.id, +// 'name', rn.name, +// 'fqn', rn.fqn +// ) +// WHEN a.namespace_id IS NULL THEN NULL +// ELSE JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', ns_fqns.fqn +// ) +// END AS namespace // FROM actions a +// LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +// LEFT JOIN resolved_namespace rn ON TRUE // WHERE -// ($1::uuid IS NULL OR a.id = $1::uuid) -// AND ($2::text IS NULL OR a.name = $2::text) +// ( +// ($2::uuid IS NOT NULL AND a.id = $2::uuid) +// OR +// ( +// $1::text IS NOT NULL +// AND a.name = $1::text +// AND ( +// (rn.id IS NOT NULL AND (a.namespace_id = rn.id OR a.namespace_id IS NULL)) +// OR +// (rn.id IS NULL AND a.namespace_id IS NULL) +// ) +// ) +// ) +// ORDER BY +// CASE +// WHEN a.namespace_id = rn.id THEN 0 +// WHEN a.is_standard = TRUE THEN 1 +// ELSE 2 +// END, +// a.created_at DESC +// LIMIT 1 func (q *Queries) getAction(ctx context.Context, arg getActionParams) (getActionRow, error) { - row := q.db.QueryRow(ctx, getAction, arg.ID, arg.Name) + row := q.db.QueryRow(ctx, getAction, + arg.Name, + arg.ID, + arg.NamespaceID, + arg.NamespaceFqn, + ) var i getActionRow err := row.Scan( &i.ID, &i.Name, &i.IsStandard, &i.Metadata, + &i.Namespace, ) return i, err } const listActions = `-- name: listActions :many -WITH counted AS ( - SELECT COUNT(id) AS total FROM actions +WITH resolved_namespace AS ( + SELECT + n.id, + n.name, + fqns.fqn + FROM attribute_namespaces n + LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL + WHERE + ($3::uuid IS NOT NULL AND n.id = $3::uuid) + OR + ($4::text IS NOT NULL AND fqns.fqn = $4::text) + LIMIT 1 +), +counted AS ( + SELECT COUNT(a.id) AS total + FROM actions a + JOIN resolved_namespace rn ON TRUE + WHERE a.is_standard = TRUE OR a.namespace_id = rn.id OR a.namespace_id IS NULL ) SELECT a.id, @@ -227,33 +389,68 @@ SELECT 'updated_at', a.updated_at )) as metadata, a.is_standard, + CASE + WHEN a.namespace_id IS NULL THEN JSON_BUILD_OBJECT( + 'id', rn.id, + 'name', rn.name, + 'fqn', rn.fqn + ) + ELSE JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + END AS namespace, counted.total FROM actions a +JOIN resolved_namespace rn ON TRUE +LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL CROSS JOIN counted +WHERE a.is_standard = TRUE OR a.namespace_id = rn.id OR a.namespace_id IS NULL ORDER BY a.created_at DESC LIMIT $2 OFFSET $1 ` type listActionsParams struct { - Offset int32 `json:"offset_"` - Limit int32 `json:"limit_"` + Offset int32 `json:"offset_"` + Limit int32 `json:"limit_"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` } type listActionsRow struct { - ID string `json:"id"` - Name string `json:"name"` - Metadata []byte `json:"metadata"` - IsStandard bool `json:"is_standard"` - Total int64 `json:"total"` + ID string `json:"id"` + Name string `json:"name"` + Metadata []byte `json:"metadata"` + IsStandard bool `json:"is_standard"` + Namespace interface{} `json:"namespace"` + Total int64 `json:"total"` } // -------------------------------------------------------------- // ACTIONS // -------------------------------------------------------------- // -// WITH counted AS ( -// SELECT COUNT(id) AS total FROM actions +// WITH resolved_namespace AS ( +// SELECT +// n.id, +// n.name, +// fqns.fqn +// FROM attribute_namespaces n +// LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL +// WHERE +// ($3::uuid IS NOT NULL AND n.id = $3::uuid) +// OR +// ($4::text IS NOT NULL AND fqns.fqn = $4::text) +// LIMIT 1 +// ), +// counted AS ( +// SELECT COUNT(a.id) AS total +// FROM actions a +// JOIN resolved_namespace rn ON TRUE +// WHERE a.is_standard = TRUE OR a.namespace_id = rn.id OR a.namespace_id IS NULL // ) // SELECT // a.id, @@ -264,14 +461,35 @@ type listActionsRow struct { // 'updated_at', a.updated_at // )) as metadata, // a.is_standard, +// CASE +// WHEN a.namespace_id IS NULL THEN JSON_BUILD_OBJECT( +// 'id', rn.id, +// 'name', rn.name, +// 'fqn', rn.fqn +// ) +// ELSE JSON_BUILD_OBJECT( +// 'id', n.id, +// 'name', n.name, +// 'fqn', ns_fqns.fqn +// ) +// END AS namespace, // counted.total // FROM actions a +// JOIN resolved_namespace rn ON TRUE +// LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +// LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL // CROSS JOIN counted +// WHERE a.is_standard = TRUE OR a.namespace_id = rn.id OR a.namespace_id IS NULL // ORDER BY a.created_at DESC // LIMIT $2 // OFFSET $1 func (q *Queries) listActions(ctx context.Context, arg listActionsParams) ([]listActionsRow, error) { - rows, err := q.db.Query(ctx, listActions, arg.Offset, arg.Limit) + rows, err := q.db.Query(ctx, listActions, + arg.Offset, + arg.Limit, + arg.NamespaceID, + arg.NamespaceFqn, + ) if err != nil { return nil, err } @@ -284,6 +502,7 @@ func (q *Queries) listActions(ctx context.Context, arg listActionsParams) ([]lis &i.Name, &i.Metadata, &i.IsStandard, + &i.Namespace, &i.Total, ); err != nil { return nil, err diff --git a/service/policy/db/migrations/20260312000000_add_namespace_to_actions.sql b/service/policy/db/migrations/20260312000000_add_namespace_to_actions.sql new file mode 100644 index 0000000000..b2bdc3c37b --- /dev/null +++ b/service/policy/db/migrations/20260312000000_add_namespace_to_actions.sql @@ -0,0 +1,37 @@ +-- +goose Up +-- +goose StatementBegin + +-- Add nullable namespace_id column to actions for namespace-scoped custom actions. +-- Keep nullable for legacy custom actions and standard CRUD actions. +ALTER TABLE actions + ADD COLUMN namespace_id UUID REFERENCES attribute_namespaces(id) ON DELETE CASCADE; + +-- Drop existing global uniqueness constraint. +ALTER TABLE actions DROP CONSTRAINT actions_name_unique; + +-- Namespaced custom actions: unique name per namespace. +CREATE UNIQUE INDEX actions_namespace_name_unique + ON actions(namespace_id, name) WHERE namespace_id IS NOT NULL; + +-- Legacy/global actions (including standard CRUD actions): unique name globally. +CREATE UNIQUE INDEX actions_name_unique + ON actions(name) WHERE namespace_id IS NULL; + +-- Index for namespace-scoped action queries. +CREATE INDEX idx_actions_namespace_id + ON actions(namespace_id); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DROP INDEX IF EXISTS idx_actions_namespace_id; +DROP INDEX IF EXISTS actions_name_unique; +DROP INDEX IF EXISTS actions_namespace_name_unique; + +ALTER TABLE actions ADD CONSTRAINT actions_name_unique UNIQUE (name); + +ALTER TABLE actions DROP COLUMN IF EXISTS namespace_id; + +-- +goose StatementEnd diff --git a/service/policy/db/models.go b/service/policy/db/models.go index 8e25e5b83e..fc1d4ab2d2 100644 --- a/service/policy/db/models.go +++ b/service/policy/db/models.go @@ -64,9 +64,10 @@ type Action struct { // Whether the action is standard (proto-enum) or custom (user-defined). IsStandard bool `json:"is_standard"` // Metadata for the action (see protos for structure) - Metadata []byte `json:"metadata"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Metadata []byte `json:"metadata"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + NamespaceID pgtype.UUID `json:"namespace_id"` } // View to retrieve active public keys mapped to attribute definitions diff --git a/service/policy/db/queries/actions.sql b/service/policy/db/queries/actions.sql index 6532bfed98..7d4744cf20 100644 --- a/service/policy/db/queries/actions.sql +++ b/service/policy/db/queries/actions.sql @@ -3,8 +3,24 @@ ---------------------------------------------------------------- -- name: listActions :many -WITH counted AS ( - SELECT COUNT(id) AS total FROM actions +WITH resolved_namespace AS ( + SELECT + n.id, + n.name, + fqns.fqn + FROM attribute_namespaces n + LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL + WHERE + (sqlc.narg('namespace_id')::uuid IS NOT NULL AND n.id = sqlc.narg('namespace_id')::uuid) + OR + (sqlc.narg('namespace_fqn')::text IS NOT NULL AND fqns.fqn = sqlc.narg('namespace_fqn')::text) + LIMIT 1 +), +counted AS ( + SELECT COUNT(a.id) AS total + FROM actions a + JOIN resolved_namespace rn ON TRUE + WHERE a.is_standard = TRUE OR a.namespace_id = rn.id OR a.namespace_id IS NULL ) SELECT a.id, @@ -15,38 +31,103 @@ SELECT 'updated_at', a.updated_at )) as metadata, a.is_standard, + CASE + WHEN a.namespace_id IS NULL THEN JSON_BUILD_OBJECT( + 'id', rn.id, + 'name', rn.name, + 'fqn', rn.fqn + ) + ELSE JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + END AS namespace, counted.total FROM actions a +JOIN resolved_namespace rn ON TRUE +LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL CROSS JOIN counted +WHERE a.is_standard = TRUE OR a.namespace_id = rn.id OR a.namespace_id IS NULL ORDER BY a.created_at DESC LIMIT @limit_ OFFSET @offset_; -- name: getAction :one +WITH resolved_namespace AS ( + SELECT + n.id, + n.name, + fqns.fqn + FROM attribute_namespaces n + LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL + WHERE + (sqlc.narg('namespace_id')::uuid IS NOT NULL AND n.id = sqlc.narg('namespace_id')::uuid) + OR + (sqlc.narg('namespace_fqn')::text IS NOT NULL AND fqns.fqn = sqlc.narg('namespace_fqn')::text) + LIMIT 1 +) SELECT a.id, a.name, a.is_standard, - JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata + JSON_STRIP_NULLS(JSON_BUILD_OBJECT('labels', a.metadata -> 'labels', 'created_at', a.created_at, 'updated_at', a.updated_at)) AS metadata, + CASE + WHEN a.namespace_id IS NULL AND sqlc.narg('name')::text IS NOT NULL THEN JSON_BUILD_OBJECT( + 'id', rn.id, + 'name', rn.name, + 'fqn', rn.fqn + ) + WHEN a.namespace_id IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'id', n.id, + 'name', n.name, + 'fqn', ns_fqns.fqn + ) + END AS namespace FROM actions a +LEFT JOIN attribute_namespaces n ON a.namespace_id = n.id +LEFT JOIN attribute_fqns ns_fqns ON ns_fqns.namespace_id = n.id AND ns_fqns.attribute_id IS NULL AND ns_fqns.value_id IS NULL +LEFT JOIN resolved_namespace rn ON TRUE WHERE - (sqlc.narg('id')::uuid IS NULL OR a.id = sqlc.narg('id')::uuid) - AND (sqlc.narg('name')::text IS NULL OR a.name = sqlc.narg('name')::text); + ( + (sqlc.narg('id')::uuid IS NOT NULL AND a.id = sqlc.narg('id')::uuid) + OR + ( + sqlc.narg('name')::text IS NOT NULL + AND a.name = sqlc.narg('name')::text + AND ( + (rn.id IS NOT NULL AND (a.namespace_id = rn.id OR a.namespace_id IS NULL)) + OR + (rn.id IS NULL AND a.namespace_id IS NULL) + ) + ) + ) +ORDER BY + CASE + WHEN a.namespace_id = rn.id THEN 0 + WHEN a.is_standard = TRUE THEN 1 + ELSE 2 + END, + a.created_at DESC +LIMIT 1; -- name: createOrListActionsByName :many WITH input_actions AS ( SELECT unnest(sqlc.arg('action_names')::text[]) AS name ), new_actions AS ( - INSERT INTO actions (name, is_standard) + INSERT INTO actions (name, is_standard, namespace_id) SELECT input.name, - FALSE -- custom actions + FALSE, -- custom actions + NULL FROM input_actions input WHERE NOT EXISTS ( - SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) + SELECT 1 FROM actions a WHERE LOWER(a.name) = LOWER(input.name) AND a.namespace_id IS NULL ) - ON CONFLICT (name) DO NOTHING + ON CONFLICT (name) WHERE namespace_id IS NULL DO NOTHING RETURNING id, name, is_standard, created_at ), all_actions AS ( @@ -55,6 +136,7 @@ all_actions AS ( TRUE AS pre_existing FROM actions a JOIN input_actions input ON LOWER(a.name) = LOWER(input.name) + WHERE a.namespace_id IS NULL UNION ALL @@ -73,8 +155,23 @@ FROM all_actions ORDER BY name; -- name: createCustomAction :one -INSERT INTO actions (name, metadata, is_standard) -VALUES ($1, $2, FALSE) +WITH ns AS ( + SELECT + sqlc.narg('namespace_id')::uuid AS id, + sqlc.narg('namespace_fqn')::text AS fqn +) +INSERT INTO actions (name, metadata, is_standard, namespace_id) +SELECT + @name, + @metadata, + FALSE, + COALESCE(ns.id, fqns.namespace_id) +FROM ns +LEFT JOIN attribute_fqns fqns ON fqns.fqn = ns.fqn AND ns.id IS NULL +WHERE + (ns.id IS NOT NULL) + OR + (ns.fqn IS NOT NULL AND fqns.namespace_id IS NOT NULL) RETURNING id; -- name: updateCustomAction :execrows diff --git a/service/policy/db/registered_resources.go b/service/policy/db/registered_resources.go index 61d3109d54..a14a7965d9 100644 --- a/service/policy/db/registered_resources.go +++ b/service/policy/db/registered_resources.go @@ -585,7 +585,8 @@ func (c PolicyDBClient) createRegisteredResourceActionAttributeValues(ctx contex actionID = ident.ActionId case *registeredresources.ActionAttributeValue_ActionName: a, err := c.queries.getAction(ctx, getActionParams{ - Name: pgtypeText(strings.ToLower(ident.ActionName)), + Name: pgtypeText(strings.ToLower(ident.ActionName)), + NamespaceID: pgtypeUUID(resourceNamespaceID), }) if err != nil { return db.WrapIfKnownInvalidQueryErr(err)