diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5f69051a1..191fe4775 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3984,7 +3984,124 @@ paths: description: User have no permission to list webhook jobs of the project. '500': description: Unexpected internal errors. - + '/projects/{project_id}/immutabletagrules': + get: + summary: List all immutable tag rules of current project + description: | + This endpoint returns the immutable tag rules of a project + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + tags: + - Products + responses: + '200': + description: List project immutable tag rules successfully. + schema: + type: array + items: + $ref: '#/definitions/ImmutableTagRule' + '400': + description: Illegal format of provided ID value. + '401': + description: User need to log in first. + '403': + description: User have no permission to list immutable tag rules of the project. + '500': + description: Unexpected internal errors. + post: + summary: Add an immutable tag rule to current project + description: | + This endpoint add an immutable tag rule to the project + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: immutabletagrule + in: body + schema: + $ref: '#/definitions/ImmutableTagRule' + tags: + - Products + responses: + '200': + description: Add the immutable tag rule successfully. + '400': + description: Illegal format of provided ID value. + '401': + description: User need to log in first. + '403': + description: User have no permission to get immutable tag rule of the project. + '500': + description: Internal server errors. + '/projects/{project_id}/immutabletagrules/{id}': + put: + summary: Update the immutable tag rule or enable or disable the rule + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: id + in: path + type: integer + format: int64 + required: true + description: Immutable tag rule ID. + - name: immutabletagrule + in: body + schema: + $ref: '#/definitions/ImmutableTagRule' + tags: + - Products + responses: + '200': + description: Update the immutable tag rule successfully. + '400': + description: Illegal format of provided ID value. + '401': + description: User need to log in first. + '403': + description: User have no permission to update the immutable tag rule of the project. + '500': + description: Internal server errors. + delete: + summary: Delete the immutable tag rule. + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: id + in: path + type: integer + format: int64 + required: true + description: Immutable tag rule ID. + tags: + - Products + responses: + '200': + description: Delete the immutable tag rule successfully. + '400': + description: Illegal format of provided ID value. + '401': + description: User need to log in first. + '403': + description: User have no permission to delete immutable tags of the project. + '500': + description: Internal server errors. '/retentions/metadatas': get: summary: Get Retention Metadatas @@ -6264,10 +6381,22 @@ definitions: type: integer retained: type: integer - QuotaSwitcher: type: object properties: enabled: type: boolean description: The quota is enable or disable + ImmutableTagRule: + type: object + properties: + id: + type: integer + format: int64 + project_id: + type: integer + format: int64 + tag_filter: + type: string + enabled: + type: boolean diff --git a/src/common/dao/immutable.go b/src/common/dao/immutable.go index 5b279b1fb..f8fa3a3fe 100644 --- a/src/common/dao/immutable.go +++ b/src/common/dao/immutable.go @@ -15,7 +15,8 @@ func CreateImmutableRule(ir *models.ImmutableRule) (int64, error) { } // UpdateImmutableRule update the immutable rules -func UpdateImmutableRule(projectID int, ir *models.ImmutableRule) (int64, error) { +func UpdateImmutableRule(projectID int64, ir *models.ImmutableRule) (int64, error) { + ir.ProjectID = projectID o := GetOrmer() return o.Update(ir, "TagFilter") } diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index 6cadbddef..aa549116a 100755 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -49,6 +49,7 @@ const ( ResourceReplicationTask = Resource("replication-task") ResourceRepository = Resource("repository") ResourceTagRetention = Resource("tag-retention") + ResourceImmutableTag = Resource("immutable-tag") ResourceRepositoryLabel = Resource("repository-label") ResourceRepositoryTag = Resource("repository-tag") ResourceRepositoryTagLabel = Resource("repository-tag-label") diff --git a/src/common/rbac/project/util.go b/src/common/rbac/project/util.go index 3de3f5810..ce69800aa 100644 --- a/src/common/rbac/project/util.go +++ b/src/common/rbac/project/util.go @@ -95,6 +95,11 @@ var ( {Resource: rbac.ResourceTagRetention, Action: rbac.ActionList}, {Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionCreate}, {Resource: rbac.ResourceLabel, Action: rbac.ActionRead}, {Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate}, diff --git a/src/common/rbac/project/visitor_role.go b/src/common/rbac/project/visitor_role.go index 36202a602..651252cdb 100755 --- a/src/common/rbac/project/visitor_role.go +++ b/src/common/rbac/project/visitor_role.go @@ -68,6 +68,11 @@ var ( {Resource: rbac.ResourceTagRetention, Action: rbac.ActionList}, {Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate}, {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete}, {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList}, @@ -153,6 +158,11 @@ var ( {Resource: rbac.ResourceTagRetention, Action: rbac.ActionList}, {Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate}, {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete}, {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList}, diff --git a/src/core/api/api_test.go b/src/core/api/api_test.go index f8e1ccdd0..ae1970ab1 100644 --- a/src/core/api/api_test.go +++ b/src/core/api/api_test.go @@ -180,6 +180,7 @@ func runCodeCheckingCases(t *testing.T, cases ...*codeCheckingCase) { if c.postFunc != nil { if err := c.postFunc(resp); err != nil { t.Logf("error in running post function: %v", err) + t.Error(err) } } } diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index be9a62050..7cab5f769 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -177,7 +177,8 @@ func init() { beego.Router("/api/projects/:pid([0-9]+)/webhook/policies/test", &NotificationPolicyAPI{}, "post:Test") beego.Router("/api/projects/:pid([0-9]+)/webhook/lasttrigger", &NotificationPolicyAPI{}, "get:ListGroupByEventType") beego.Router("/api/projects/:pid([0-9]+)/webhook/jobs/", &NotificationJobAPI{}, "get:List") - + beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules", &ImmutableTagRuleAPI{}, "get:List;post:Post") + beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules/:id([0-9]+)", &ImmutableTagRuleAPI{}) // Charts are controlled under projects chartRepositoryAPIType := &ChartRepositoryAPI{} beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus") diff --git a/src/core/api/immutabletagrule.go b/src/core/api/immutabletagrule.go new file mode 100644 index 000000000..d88fe3e45 --- /dev/null +++ b/src/core/api/immutabletagrule.go @@ -0,0 +1,166 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/pkg/immutabletag" + "github.com/goharbor/harbor/src/pkg/retention/policy/rule" +) + +// ImmutableTagRuleAPI ... +type ImmutableTagRuleAPI struct { + BaseController + manager immutabletag.RuleManager + projectID int64 + ID int64 +} + +// Prepare validates the user and projectID +func (itr *ImmutableTagRuleAPI) Prepare() { + itr.BaseController.Prepare() + if !itr.SecurityCtx.IsAuthenticated() { + itr.SendUnAuthorizedError(errors.New("Unauthorized")) + return + } + + pid, err := itr.GetInt64FromPath(":pid") + if err != nil || pid <= 0 { + text := "invalid project ID: " + if err != nil { + text += err.Error() + } else { + text += fmt.Sprintf("%d", pid) + } + itr.SendBadRequestError(errors.New(text)) + return + } + itr.projectID = pid + + ruleID, err := itr.GetInt64FromPath(":id") + if err == nil || ruleID > 0 { + itr.ID = ruleID + } + + itr.manager = immutabletag.NewDefaultRuleManager() + + if strings.EqualFold(itr.Ctx.Request.Method, "get") { + if !itr.requireAccess(rbac.ActionList) { + return + } + } else if strings.EqualFold(itr.Ctx.Request.Method, "put") { + if !itr.requireAccess(rbac.ActionUpdate) { + return + } + } else if strings.EqualFold(itr.Ctx.Request.Method, "post") { + if !itr.requireAccess(rbac.ActionCreate) { + return + } + + } else if strings.EqualFold(itr.Ctx.Request.Method, "delete") { + if !itr.requireAccess(rbac.ActionDelete) { + return + } + } +} + +func (itr *ImmutableTagRuleAPI) requireAccess(action rbac.Action) bool { + return itr.RequireProjectAccess(itr.projectID, action, rbac.ResourceImmutableTag) +} + +// List list all immutable tag rules of current project +func (itr *ImmutableTagRuleAPI) List() { + rules, err := itr.manager.QueryImmutableRuleByProjectID(itr.projectID) + if err != nil { + itr.SendInternalServerError(err) + return + } + itr.WriteJSONData(rules) +} + +// Post create immutable tag rule +func (itr *ImmutableTagRuleAPI) Post() { + ir := &models.ImmutableRule{} + if err := itr.DecodeJSONReq(ir); err != nil { + itr.SendBadRequestError(fmt.Errorf("the filter must be a valid json, failed to parse json, error %+v", err)) + return + } + + if !isValidSelectorJSON(ir.TagFilter) { + itr.SendBadRequestError(fmt.Errorf("the filter should be a valid json")) + return + } + + ir.ProjectID = itr.projectID + id, err := itr.manager.CreateImmutableRule(ir) + if err != nil { + itr.SendInternalServerError(err) + return + } + itr.Redirect(http.StatusCreated, strconv.FormatInt(id, 10)) + +} + +// Delete delete immutable tag rule +func (itr *ImmutableTagRuleAPI) Delete() { + if itr.ID <= 0 { + itr.SendBadRequestError(fmt.Errorf("invalid immutable rule id %d", itr.ID)) + return + } + _, err := itr.manager.DeleteImmutableRule(itr.ID) + if err != nil { + itr.SendInternalServerError(err) + return + } +} + +// Put update an immutable tag rule +func (itr *ImmutableTagRuleAPI) Put() { + ir := &models.ImmutableRule{} + if err := itr.DecodeJSONReq(ir); err != nil { + itr.SendInternalServerError(err) + return + } + ir.ID = itr.ID + ir.ProjectID = itr.projectID + + if itr.ID <= 0 { + itr.SendBadRequestError(fmt.Errorf("invalid immutable rule id %d", itr.ID)) + return + } + if len(ir.TagFilter) == 0 { + if _, err := itr.manager.EnableImmutableRule(itr.ID, ir.Enabled); err != nil { + itr.SendInternalServerError(err) + return + } + } else { + + if !isValidSelectorJSON(ir.TagFilter) { + itr.SendBadRequestError(fmt.Errorf("the filter should be a valid json")) + return + } + + if _, err := itr.manager.UpdateImmutableRule(itr.ID, ir); err != nil { + itr.SendInternalServerError(err) + return + } + } + +} + +func isValidSelectorJSON(filter string) bool { + tagSector := &rule.Metadata{} + err := json.Unmarshal([]byte(filter), tagSector) + if err != nil { + log.Errorf("The json is %v", filter) + return false + } + return true +} diff --git a/src/core/api/immutabletagrule_test.go b/src/core/api/immutabletagrule_test.go new file mode 100644 index 000000000..7255b90ce --- /dev/null +++ b/src/core/api/immutabletagrule_test.go @@ -0,0 +1,264 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/pkg/immutabletag" +) + +func TestImmutableTagRuleAPI_List(t *testing.T) { + + tagFilter := `{ + "id":0, + "priority":0, + "disabled":false, + "action":"immutable", + "template":"immutable_template", + "tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"**"}], + "scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"**"}]} + }` + + mgr := immutabletag.NewDefaultRuleManager() + id, err := mgr.CreateImmutableRule(&models.ImmutableRule{ProjectID: 1, TagFilter: tagFilter}) + if err != nil { + t.Error(err) + } + defer mgr.DeleteImmutableRule(id) + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/1/immutabletagrules", + }, + code: http.StatusUnauthorized, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/1/immutabletagrules", + credential: admin, + }, + postFunc: func(responseRecorder *httptest.ResponseRecorder) error { + var rules []models.ImmutableRule + err := json.Unmarshal([]byte(responseRecorder.Body.String()), &rules) + if err != nil { + return err + } + if len(rules) <= 0 { + return fmt.Errorf("no rules found") + } + if rules[0].TagFilter != tagFilter { + return fmt.Errorf("rule is not expected. actual: %v", responseRecorder.Body.String()) + } + return nil + }, + code: http.StatusOK, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/1/immutabletagrules", + credential: projAdmin, + }, + code: http.StatusOK, + }, + // 403 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/1/immutabletagrules", + credential: projGuest, + }, + code: http.StatusForbidden, + }, + } + runCodeCheckingCases(t, cases...) + +} + +func TestImmutableTagRuleAPI_Post(t *testing.T) { + + tagFilter := `{ + "id":0, + "priority":0, + "disabled":false, + "action":"immutable", + "template":"immutable_template", + "tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"**"}], + "scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"**"}]} +}` + body := &models.ImmutableRule{ProjectID: 1, TagFilter: tagFilter} + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodPost, + url: "/api/projects/1/immutabletagrules", + bodyJSON: body, + }, + code: http.StatusUnauthorized, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodPost, + url: "/api/projects/1/immutabletagrules", + credential: admin, + bodyJSON: body, + }, + code: http.StatusCreated, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodPost, + url: "/api/projects/1/immutabletagrules", + credential: projAdmin, + bodyJSON: body, + }, + code: http.StatusCreated, + }, + // 403 + { + request: &testingRequest{ + method: http.MethodPost, + url: "/api/projects/1/immutabletagrules", + credential: projGuest, + bodyJSON: body, + }, + code: http.StatusForbidden, + }, + } + runCodeCheckingCases(t, cases...) + +} + +func TestImmutableTagRuleAPI_Put(t *testing.T) { + tagFilter := `{ + "id":0, + "priority":0, + "disabled":false, + "action":"immutable", + "template":"immutable_template", + "tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"**"}], + "scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"**"}]} +}` + tagFilter2 := `{ + "id":0, + "priority":0, + "disabled":false, + "action":"immutable", + "template":"immutable_template", + "tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"release-1.6.0"}], + "scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"regids"}]} +}` + + mgr := immutabletag.NewDefaultRuleManager() + id, err := mgr.CreateImmutableRule(&models.ImmutableRule{ProjectID: 1, TagFilter: tagFilter}) + if err != nil { + t.Error(err) + } + defer mgr.DeleteImmutableRule(id) + url := fmt.Sprintf("/api/projects/1/immutabletagrules/%d", id) + body := &models.ImmutableRule{ID: id, ProjectID: 1, TagFilter: tagFilter2} + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + bodyJSON: body, + }, + code: http.StatusUnauthorized, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + credential: admin, + bodyJSON: body, + }, + code: http.StatusOK, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + credential: projAdmin, + bodyJSON: body, + }, + code: http.StatusOK, + }, + // 403 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + credential: projGuest, + bodyJSON: body, + }, + code: http.StatusForbidden, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestImmutableTagRuleAPI_Delete(t *testing.T) { + tagFilter := `{ + "id":0, + "priority":0, + "disabled":false, + "action":"immutable", + "template":"immutable_template", + "tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"**"}], + "scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"**"}]} +}` + mgr := immutabletag.NewDefaultRuleManager() + id, err := mgr.CreateImmutableRule(&models.ImmutableRule{ProjectID: 1, TagFilter: tagFilter}) + if err != nil { + t.Error(err) + } + defer mgr.DeleteImmutableRule(id) + + url := fmt.Sprintf("/api/projects/1/immutabletagrules/%d", id) + + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodDelete, + url: url, + }, + code: http.StatusUnauthorized, + }, + // 403 + { + request: &testingRequest{ + method: http.MethodDelete, + url: url, + credential: projGuest, + }, + code: http.StatusForbidden, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodDelete, + url: url, + credential: projAdmin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} diff --git a/src/core/router.go b/src/core/router.go index 7afe5f8f8..b7ee2736d 100755 --- a/src/core/router.go +++ b/src/core/router.go @@ -121,6 +121,9 @@ func initRouters() { beego.Router("/api/projects/:pid([0-9]+)/webhook/jobs/", &api.NotificationJobAPI{}, "get:List") + beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules", &api.ImmutableTagRuleAPI{}, "get:List;post:Post") + beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules/:id([0-9]+)", &api.ImmutableTagRuleAPI{}) + beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put") beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put") beego.Router("/api/statistics", &api.StatisticAPI{}) @@ -164,6 +167,8 @@ func initRouters() { beego.Router("/api/retentions/:id/executions", &api.RetentionAPI{}, "get:ListRetentionExecs") beego.Router("/api/retentions/:id/executions/:eid/tasks", &api.RetentionAPI{}, "get:ListRetentionExecTasks") beego.Router("/api/retentions/:id/executions/:eid/tasks/:tid", &api.RetentionAPI{}, "get:GetRetentionExecTaskLog") + beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules", &api.ImmutableTagRuleAPI{}, "get:List;post:Post") + beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules/:id([0-9]+)", &api.ImmutableTagRuleAPI{}) beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle") diff --git a/src/pkg/immutabletag/rulemanager.go b/src/pkg/immutabletag/rulemanager.go new file mode 100644 index 000000000..2497e5a76 --- /dev/null +++ b/src/pkg/immutabletag/rulemanager.go @@ -0,0 +1,59 @@ +package immutabletag + +import ( + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" +) + +// RuleManager ... +type RuleManager interface { + // CreateImmutableRule creates the Immutable Rule + CreateImmutableRule(ir *models.ImmutableRule) (int64, error) + // UpdateImmutableRule update the immutable rules + UpdateImmutableRule(projectID int64, ir *models.ImmutableRule) (int64, error) + // EnableImmutableRule enable/disable immutable rules + EnableImmutableRule(id int64, enabled bool) (int64, error) + // GetImmutableRule get immutable rule + GetImmutableRule(id int64) (*models.ImmutableRule, error) + // QueryImmutableRuleByProjectID get all immutable rule by project + QueryImmutableRuleByProjectID(projectID int64) ([]models.ImmutableRule, error) + // QueryEnabledImmutableRuleByProjectID get all enabled immutable rule by project + QueryEnabledImmutableRuleByProjectID(projectID int64) ([]models.ImmutableRule, error) + // DeleteImmutableRule delete the immutable rule + DeleteImmutableRule(id int64) (int64, error) +} + +type defaultRuleManager struct{} + +func (drm *defaultRuleManager) CreateImmutableRule(ir *models.ImmutableRule) (int64, error) { + return dao.CreateImmutableRule(ir) +} + +func (drm *defaultRuleManager) UpdateImmutableRule(projectID int64, ir *models.ImmutableRule) (int64, error) { + return dao.UpdateImmutableRule(projectID, ir) +} + +func (drm *defaultRuleManager) EnableImmutableRule(id int64, enabled bool) (int64, error) { + return dao.ToggleImmutableRule(id, enabled) +} + +func (drm *defaultRuleManager) GetImmutableRule(id int64) (*models.ImmutableRule, error) { + return dao.GetImmutableRule(id) +} + +func (drm *defaultRuleManager) QueryImmutableRuleByProjectID(projectID int64) ([]models.ImmutableRule, error) { + return dao.QueryImmutableRuleByProjectID(projectID) +} + +func (drm *defaultRuleManager) QueryEnabledImmutableRuleByProjectID(projectID int64) ([]models.ImmutableRule, error) { + return dao.QueryEnabledImmutableRuleByProjectID(projectID) +} + +func (drm *defaultRuleManager) DeleteImmutableRule(id int64) (int64, error) { + return dao.DeleteImmutableRule(id) +} + +// NewDefaultRuleManager return a new instance of defaultRuleManager +func NewDefaultRuleManager() RuleManager { + return &defaultRuleManager{} +}