Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go/nautobotop/api/v1alpha1/nautobot_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type NautobotSpec struct {
TenantGroupRef []ConfigMapRef `json:"tenantGroupRef,omitempty"`
TenantRef []ConfigMapRef `json:"tenantRef,omitempty"`
DeviceRef []ConfigMapRef `json:"deviceRef,omitempty"`
PermissionGroupRef []ConfigMapRef `json:"permissionGroupRef,omitempty"`
}

// NautobotStatus defines the observed state of Nautobot.
Expand Down
1 change: 1 addition & 0 deletions go/nautobotop/internal/controller/dag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func TestTopologicalSort(t *testing.T) {
{Name: "namespace", DependsOn: []string{"location", "tenant"}},
{Name: "vlan", DependsOn: []string{"vlanGroup", "location", "tenant", "role"}},
{Name: "prefix", DependsOn: []string{"namespace", "rir", "location", "vlan", "tenant", "role"}},
{Name: "permissionGroup"},
},
want: []string{
"clusterGroup", "clusterType", "deviceType", "locationTypes",
Expand Down
18 changes: 18 additions & 0 deletions go/nautobotop/internal/controller/nautobot_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

nbClient "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client"
"github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/sync"
syncadmin "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/sync/admin"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

Expand Down Expand Up @@ -116,6 +117,7 @@ func (r *NautobotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
{name: "cluster", dependsOn: []string{"clusterType", "clusterGroup", "location", "device"}, configRefs: nautobotCR.Spec.ClusterRef, syncFunc: r.syncCluster},
// Depends on: namespace, rir, location, vlan, tenant, role
{name: "prefix", dependsOn: []string{"namespace", "rir", "location", "vlan", "tenant", "role"}, configRefs: nautobotCR.Spec.PrefixRef, syncFunc: r.syncPrefix},
{name: "permissionGroup", configRefs: nautobotCR.Spec.PermissionGroupRef, syncFunc: r.syncPermissionGroup},
}

// Resolve execution order using topological sort (Kahn's algorithm)
Expand Down Expand Up @@ -298,6 +300,22 @@ func (r *NautobotReconciler) syncRack(ctx context.Context,
return nil
}

func (r *NautobotReconciler) syncPermissionGroup(ctx context.Context,
nautobotClient *nbClient.NautobotClient,
permissionGroupData map[string]string,
) error {
log := logf.FromContext(ctx)
log.Info("syncing permission groups", "count", len(permissionGroupData))
if len(permissionGroupData) == 0 {
return nil
}
syncSvc := syncadmin.NewPermissionGroupSync(nautobotClient)
if err := syncSvc.SyncAll(ctx, permissionGroupData); err != nil {
return fmt.Errorf("failed to sync permission groups: %w", err)
}
return nil
}

func (r *NautobotReconciler) syncLocation(ctx context.Context,
nautobotClient *nbClient.NautobotClient,
locationType map[string]string,
Expand Down
78 changes: 78 additions & 0 deletions go/nautobotop/internal/nautobot/admin/group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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 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()
if err != nil {
bodyString := helpers.ReadResponseBody(resp)
s.client.AddReport("createNewGroup", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString)
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()
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 {
return nil
}
return &list.Results[0]
}

func (s *GroupService) ListAll(ctx context.Context) []nb.Group {
return helpers.PaginatedList(
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
}
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)
if existing != nil {
return existing, nil
}

log.Info("group not found, creating", "name", name)
req := nb.GroupRequest{Name: name}
return s.Create(ctx, req)
}
155 changes: 155 additions & 0 deletions go/nautobotop/internal/nautobot/admin/permission.go
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()
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
}
48 changes: 48 additions & 0 deletions go/nautobotop/internal/nautobot/models/admin/permissionGroup.go
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
}
Loading
Loading