-
Notifications
You must be signed in to change notification settings - Fork 10
feat: adding Nautobot Admin Groups and Permission sync #2157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package admin | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
|
|
||
| "github.com/charmbracelet/log" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A suggestion, we should be using "k8s.io/klog/v2". This has more chance of being maintained and up to date with latest vulnerabilities. |
||
| nb "github.com/nautobot/go-nautobot/v3" | ||
| "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" | ||
| "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" | ||
| ) | ||
|
|
||
| type GroupService struct { | ||
| client *client.NautobotClient | ||
| } | ||
|
|
||
| func NewGroupService(nautobotClient *client.NautobotClient) *GroupService { | ||
| return &GroupService{ | ||
| client: nautobotClient, | ||
| } | ||
| } | ||
|
|
||
| func (s *GroupService) Create(ctx context.Context, req nb.GroupRequest) (*nb.Group, error) { | ||
| group, resp, err := s.client.APIClient.UsersAPI.UsersGroupsCreate(ctx).GroupRequest(req).Execute() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again an observation! This calls multiple nested functions. We should try to make this more modular. |
||
| if err != nil { | ||
| bodyString := helpers.ReadResponseBody(resp) | ||
| s.client.AddReport("createNewGroup", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this different from logging? |
||
| return nil, err | ||
| } | ||
| log.Info("CreateGroup", "created group", group.Name) | ||
| return group, nil | ||
| } | ||
|
|
||
| func (s *GroupService) GetByName(ctx context.Context, name string) *nb.Group { | ||
| list, resp, err := s.client.APIClient.UsersAPI.UsersGroupsList(ctx).Name([]string{name}).Execute() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same observation, this call should be more modular. |
||
| if err != nil { | ||
| bodyString := helpers.ReadResponseBody(resp) | ||
| s.client.AddReport("GetGroupByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString) | ||
| return nil | ||
| } | ||
| if list == nil || len(list.Results) == 0 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks slightly strange, if list is nil, it should be returned as an empty list instead of nil. |
||
| return nil | ||
| } | ||
| return &list.Results[0] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If only first element is to be returned, line 35 should be calling a function where it gets that one particular element. We should not be processing the entire list here. |
||
| } | ||
|
|
||
| func (s *GroupService) ListAll(ctx context.Context) []nb.Group { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Function name should be |
||
| return helpers.PaginatedList( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return should not hold a function call. |
||
| ctx, | ||
| func(ctx context.Context, limit, offset int32) ([]nb.Group, int32, *http.Response, error) { | ||
| list, resp, err := s.client.APIClient.UsersAPI.UsersGroupsList(ctx). | ||
| Limit(limit). | ||
| Offset(offset). | ||
| Execute() | ||
| if err != nil { | ||
| return nil, 0, resp, err | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of returning 4 items separately, |
||
| } | ||
| if list == nil { | ||
| return nil, 0, resp, nil | ||
| } | ||
| return list.Results, list.Count, resp, nil | ||
| }, | ||
| s.client.AddReport, | ||
| "ListAllGroups", | ||
| ) | ||
| } | ||
|
|
||
| // GetOrCreate fetches a group by name or creates it if it doesn't exist. | ||
| func (s *GroupService) GetOrCreate(ctx context.Context, name string) (*nb.Group, error) { | ||
| existing := s.GetByName(ctx, name) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if existing != nil { | ||
| return existing, nil | ||
| } | ||
|
|
||
| log.Info("group not found, creating", "name", name) | ||
| req := nb.GroupRequest{Name: name} | ||
| return s.Create(ctx, req) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| package admin | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
|
|
||
| "github.com/charmbracelet/log" | ||
| nb "github.com/nautobot/go-nautobot/v3" | ||
| "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" | ||
| "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" | ||
| ) | ||
|
|
||
| type PermissionService struct { | ||
| client *client.NautobotClient | ||
| } | ||
|
|
||
| func NewPermissionService(nautobotClient *client.NautobotClient) *PermissionService { | ||
| return &PermissionService{ | ||
| client: nautobotClient, | ||
| } | ||
| } | ||
|
|
||
| func (s *PermissionService) Create(ctx context.Context, req nb.ObjectPermissionRequest) (*nb.ObjectPermission, error) { | ||
| perm, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsCreate(ctx).ObjectPermissionRequest(req).Execute() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, we can make this call more modular. |
||
| if err != nil { | ||
| bodyString := helpers.ReadResponseBody(resp) | ||
| s.client.AddReport("createNewPermission", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString) | ||
| return nil, err | ||
| } | ||
| log.Info("CreatePermission", "created permission", perm.Name) | ||
| return perm, nil | ||
| } | ||
|
|
||
| func (s *PermissionService) GetByName(ctx context.Context, name string) *nb.ObjectPermission { | ||
| list, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsList(ctx).Name([]string{name}).Execute() | ||
| if err != nil { | ||
| bodyString := helpers.ReadResponseBody(resp) | ||
| s.client.AddReport("GetPermissionByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString) | ||
| return nil | ||
| } | ||
| if list == nil || len(list.Results) == 0 { | ||
| return nil | ||
| } | ||
| return &list.Results[0] | ||
| } | ||
|
|
||
| func (s *PermissionService) GetByID(ctx context.Context, id string) *nb.ObjectPermission { | ||
| if id == "" { | ||
| return nil | ||
| } | ||
| list, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsList(ctx).Id([]string{id}).Execute() | ||
| if err != nil { | ||
| bodyString := helpers.ReadResponseBody(resp) | ||
| s.client.AddReport("GetPermissionByID", "failed to get", "id", id, "error", err.Error(), "response_body", bodyString) | ||
| return nil | ||
| } | ||
| if list == nil || len(list.Results) == 0 { | ||
| return nil | ||
| } | ||
| return &list.Results[0] | ||
| } | ||
|
|
||
| func (s *PermissionService) ListAll(ctx context.Context) []nb.ObjectPermission { | ||
| return helpers.PaginatedList( | ||
| ctx, | ||
| func(ctx context.Context, limit, offset int32) ([]nb.ObjectPermission, int32, *http.Response, error) { | ||
| list, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsList(ctx). | ||
| Limit(limit). | ||
| Offset(offset). | ||
| Execute() | ||
| if err != nil { | ||
| return nil, 0, resp, err | ||
| } | ||
| if list == nil { | ||
| return nil, 0, resp, nil | ||
| } | ||
| return list.Results, list.Count, resp, nil | ||
| }, | ||
| s.client.AddReport, | ||
| "ListAllPermissions", | ||
| ) | ||
| } | ||
|
|
||
| // PermissionUpdatePayload represents the desired state of a permission. | ||
| type PermissionUpdatePayload struct { | ||
| Name string `json:"name"` | ||
| Description string `json:"description"` | ||
| Enabled bool `json:"enabled"` | ||
| Actions interface{} `json:"actions"` | ||
| ObjectTypes []string `json:"object_types"` | ||
| Constraints interface{} `json:"constraints"` | ||
| Groups []GroupRef `json:"groups"` | ||
| } | ||
|
|
||
| // GroupRef is a minimal group reference for the permission update payload. | ||
| type GroupRef struct { | ||
| ID int32 `json:"id"` | ||
| } | ||
|
|
||
| // Update performs a full PUT via the SDK client | ||
| func (s *PermissionService) Update(ctx context.Context, id string, payload PermissionUpdatePayload) error { | ||
| // Build the SDK request | ||
| req := *nb.NewObjectPermissionRequest(payload.ObjectTypes, payload.Name, payload.Actions) | ||
| req.Enabled = nb.PtrBool(payload.Enabled) | ||
| if payload.Description != "" { | ||
| req.Description = nb.PtrString(payload.Description) | ||
| } | ||
|
|
||
| // Set groups | ||
| groups := make([]nb.ApprovalWorkflowStageResponseApprovalWorkflowStage, 0, len(payload.Groups)) | ||
| for _, g := range payload.Groups { | ||
| gid := g.ID | ||
| groups = append(groups, nb.ApprovalWorkflowStageResponseApprovalWorkflowStage{ | ||
| Id: &nb.ApprovalWorkflowApprovalWorkflowDefinitionId{Int32: &gid}, | ||
| }) | ||
| } | ||
| req.Groups = groups | ||
|
|
||
| if payload.Constraints != nil { | ||
| req.Constraints = payload.Constraints | ||
| } else { | ||
| req.AdditionalProperties = map[string]interface{}{ | ||
| "constraints": nil, | ||
| } | ||
| } | ||
|
|
||
| perm, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsUpdate(ctx, id).ObjectPermissionRequest(req).Execute() | ||
| if err != nil { | ||
| bodyString := helpers.ReadResponseBody(resp) | ||
| s.client.AddReport("UpdatePermission", "failed to update", "id", id, "model", payload.Name, "error", err.Error(), "response_body", bodyString) | ||
| return err | ||
| } | ||
| log.Info("successfully updated permission", "id", id, "model", perm.GetName()) | ||
| return nil | ||
| } | ||
|
|
||
| func (s *PermissionService) Destroy(ctx context.Context, id string) error { | ||
| owned, err := s.client.IsCreatedByUser(ctx, id) | ||
| if err != nil { | ||
| s.client.AddReport("DestroyPermission", "failed to check ownership", "id", id, "error", err.Error()) | ||
| return err | ||
| } | ||
| if !owned { | ||
| log.Warn("skipping destroy, object not created by user", "id", id, "user", s.client.Username) | ||
| return nil | ||
| } | ||
|
|
||
| resp, err := s.client.APIClient.UsersAPI.UsersPermissionsDestroy(ctx, id).Execute() | ||
| if err != nil { | ||
| bodyString := helpers.ReadResponseBody(resp) | ||
| s.client.AddReport("DestroyPermission", "failed to destroy", "id", id, "error", err.Error(), "response_body", bodyString) | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package admin | ||
|
|
||
| // Permission represents a single Nautobot ObjectPermission with its group assignments. | ||
| type Permission struct { | ||
| Name string `json:"name" yaml:"name"` | ||
| Description string `json:"description" yaml:"description"` | ||
| Enabled *bool `json:"enabled" yaml:"enabled"` | ||
| CanView bool `json:"can_view" yaml:"can_view"` | ||
| CanAdd bool `json:"can_add" yaml:"can_add"` | ||
| CanChange bool `json:"can_change" yaml:"can_change"` | ||
| CanDelete bool `json:"can_delete" yaml:"can_delete"` | ||
| AdditionalActions []string `json:"additional_actions" yaml:"additional_actions"` | ||
| ObjectTypes []string `json:"object_types" yaml:"object_types"` | ||
| Groups []string `json:"groups" yaml:"groups"` | ||
| Constraints string `json:"constraints" yaml:"constraints"` | ||
| } | ||
|
|
||
| // PermissionGroupConfig represents the top-level structure for the permissions configmap. | ||
| type PermissionGroupConfig struct { | ||
| Permissions []Permission `json:"permissions" yaml:"permissions"` | ||
| } | ||
|
|
||
| // BuildActions constructs the actions list from the boolean flags and additional actions. | ||
| func (p *Permission) BuildActions() []string { | ||
| var actions []string | ||
| if p.CanView { | ||
| actions = append(actions, "view") | ||
| } | ||
| if p.CanAdd { | ||
| actions = append(actions, "add") | ||
| } | ||
| if p.CanChange { | ||
| actions = append(actions, "change") | ||
| } | ||
| if p.CanDelete { | ||
| actions = append(actions, "delete") | ||
| } | ||
| actions = append(actions, p.AdditionalActions...) | ||
| return actions | ||
| } | ||
|
|
||
| // IsEnabled returns whether the permission is enabled (defaults to true if not set). | ||
| func (p *Permission) IsEnabled() bool { | ||
| if p.Enabled == nil { | ||
| return true | ||
| } | ||
| return *p.Enabled | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think log statements should be at the end of the code. In case of count zero, since this function is later on calling SyncAll, (len(permissionGroupData)) at the end should be logged.