Skip to content
Merged
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
!/resources/control-scripts
!/resources/sample*.yaml
/scripts/push_bin.sh
/scripts/.web_console_dir
binaries/*
builds/*
.vscode/*
Expand Down
2 changes: 2 additions & 0 deletions cmd/helper/server_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ func (s *Server) setupInitialUser() {
Password: hashedPassword,
FullName: "Admin User",
Email: "admin@example.com",
Policies: []string{"admin"},
Disabled: false,
}
err = s.api.User().Save(adminUser)
if err != nil {
Expand Down
770 changes: 770 additions & 0 deletions docs/access-control.md

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions pkg/api/backup/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ func New(ctx context.Context, logger *zap.Logger, backupRestore *backupTY.Backup

// List by filter and pagination
func (bk *BackupAPI) List(filters []storageTY.Filter, pagination *storageTY.Pagination) (*storageTY.Result, error) {
if pagination == nil {
pagination = &storageTY.Pagination{
Limit: 10,
Offset: 0,
SortBy: []storageTY.Sort{{Field: "id", OrderBy: storageTY.SortByASC}},
}
}

files, err := bk.GetBackupFilesList()
if err != nil {
return nil, err
Expand All @@ -42,14 +50,6 @@ func (bk *BackupAPI) List(filters []storageTY.Filter, pagination *storageTY.Pagi
finalList := make([]interface{}, 0)
totalCount := int64(0)
if len(files) > 0 {
if pagination == nil {
pagination = &storageTY.Pagination{
Limit: 10,
Offset: 0,
SortBy: []storageTY.Sort{{Field: "id", OrderBy: storageTY.SortByASC}},
}
}

// filter and then sort the files
filteredFiles := filterUtils.Filter(files, filters, false)
sortedFiles, count := filterUtils.Sort(filteredFiles, pagination)
Expand Down
65 changes: 38 additions & 27 deletions pkg/api/backup/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,42 +63,53 @@ func (bk *BackupAPI) RunOnDemandBackup(input *backupTY.OnDemandBackupConfig) err

// GetBackupFilesList details
func (bk *BackupAPI) GetBackupFilesList() ([]interface{}, error) {
exportedFiles := make([]interface{}, 0)

locationsSettings, err := bk.settingsAPI.GetBackupLocations()
if err != nil {
return nil, err
// No locations configured (or settings missing): return empty list, not an error.
// UI calls GET /api/backup on the backup page; 500 here breaks the whole page.
bk.logger.Debug("backup locations not available", zap.Error(err))
return exportedFiles, nil
}

locations := locationsSettings.Locations

exportedFiles := make([]interface{}, 0)

for _, location := range locations {
if location.Type == backupUtil.ProviderDisk {
diskLocation := &backupTY.BackupLocationDisk{}
err = utils.MapToStruct(utils.TagNameNone, location.Config, diskLocation)
if err != nil {
return exportedFiles, err
}
rawFiles, err := utils.ListFiles(diskLocation.TargetDirectory)
if err != nil {
return exportedFiles, err
if location.Type != backupUtil.ProviderDisk {
continue
}
diskLocation := &backupTY.BackupLocationDisk{}
err = utils.MapToStruct(utils.TagNameNone, location.Config, diskLocation)
if err != nil {
bk.logger.Warn("skip backup location: invalid config", zap.String("location", location.Name), zap.Error(err))
continue
}
if strings.TrimSpace(diskLocation.TargetDirectory) == "" {
bk.logger.Debug("skip backup location: empty target directory", zap.String("location", location.Name))
continue
}
rawFiles, err := utils.ListFiles(diskLocation.TargetDirectory)
if err != nil {
// Do not fail the whole list if one path is missing/unreadable
bk.logger.Warn("skip backup location: cannot list files", zap.String("location", location.Name), zap.String("dir", diskLocation.TargetDirectory), zap.Error(err))
continue
}
for _, rawFile := range rawFiles {
if rawFile.IsDir || !strings.Contains(rawFile.Name, backupUtil.BackupIdentifier) {
continue
}
for _, rawFile := range rawFiles {
if rawFile.IsDir || !strings.Contains(rawFile.Name, backupUtil.BackupIdentifier) {
continue
}
exportedFile := backupTY.BackupFile{
ID: rawFile.FullPath,
LocationName: location.Name,
ProviderType: location.Type,
Directory: diskLocation.TargetDirectory,
FileName: rawFile.Name,
FileSize: rawFile.Size,
FullPath: rawFile.FullPath,
ModifiedOn: rawFile.ModifiedTime,
}
exportedFiles = append(exportedFiles, exportedFile)
exportedFile := backupTY.BackupFile{
ID: rawFile.FullPath,
LocationName: location.Name,
ProviderType: location.Type,
Directory: diskLocation.TargetDirectory,
FileName: rawFile.Name,
FileSize: rawFile.Size,
FullPath: rawFile.FullPath,
ModifiedOn: rawFile.ModifiedTime,
}
exportedFiles = append(exportedFiles, exportedFile)
}
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/api/entities/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
gateway "github.com/mycontroller-org/server/v2/pkg/api/gateway"
handler "github.com/mycontroller-org/server/v2/pkg/api/handler"
node "github.com/mycontroller-org/server/v2/pkg/api/node"
policy "github.com/mycontroller-org/server/v2/pkg/api/policy"
schedule "github.com/mycontroller-org/server/v2/pkg/api/schedule"
serviceToken "github.com/mycontroller-org/server/v2/pkg/api/service_token"
settings "github.com/mycontroller-org/server/v2/pkg/api/settings"
Expand Down Expand Up @@ -116,6 +117,10 @@ func (a *API) Node() *node.NodeAPI {
return node.New(a.ctx, a.logger, a.storage, a.bus)
}

func (a *API) Policy() *policy.API {
return policy.New(a.ctx, a.logger, a.storage)
}

func (a *API) Schedule() *schedule.ScheduleAPI {
return schedule.New(a.ctx, a.logger, a.storage, a.bus)
}
Expand Down
131 changes: 131 additions & 0 deletions pkg/api/policy/action_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package policy

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

policyTY "github.com/mycontroller-org/server/v2/pkg/types/policy"
webHandlerTY "github.com/mycontroller-org/server/v2/pkg/types/web_handler"
)

// maxActionBodyBytes caps the body we buffer while authorizing action requests.
const maxActionBodyBytes = 1 << 20 // 1 MiB

// AuthorizeActionRequest enforces access for the /api/action* endpoints.
//
// These endpoints take their targets from the query string or the request body,
// so a bare "action" resource in a policy must not be enough - every target is
// checked individually (like quickid), otherwise /api/action becomes a write
// channel into every field/node in the system.
//
// GET /api/action/node?id=<uuid>&id=<uuid> -> action on node:<gatewayId.nodeId> for each id
// GET /api/action/gateway?id=<id> -> action on gateway:<id> for each id
// GET /api/action?resource=<quickId> -> action on the quick id target
// POST /api/action [{resource: <quickId>}] -> action on every quick id in the body
//
// Any denied target fails the whole request.
func (a *API) AuthorizeActionRequest(subject Subject, r *http.Request) error {
path := strings.TrimSuffix(r.URL.Path, "/")

switch {
case strings.HasPrefix(path, "/api/action/node"):
return a.allowedActionOnIDs(subject, policyTY.ResourceNode, r.URL.Query()["id"])
case strings.HasPrefix(path, "/api/action/gateway"):
return a.allowedActionOnIDs(subject, policyTY.ResourceGateway, r.URL.Query()["id"])
}

quickIDs := r.URL.Query()[keyResourceParam]
if r.Method == http.MethodPost {
bodyQuickIDs, err := a.actionQuickIDsFromBody(r)
if err != nil {
return err
}
quickIDs = append(quickIDs, bodyQuickIDs...)
}

if len(quickIDs) == 0 {
// no identifiable target: require the generic action capability
return a.Allowed(subject, policyTY.ActionAction, policyTY.ResourceAction)
}

checked := 0
for _, quickID := range quickIDs {
quickID = strings.TrimSpace(quickID)
if quickID == "" {
continue
}
resource, err := ResourceFromQuickID(quickID)
if err != nil {
return err
}
if err := a.Allowed(subject, policyTY.ActionAction, resource); err != nil {
return fmt.Errorf("action on %s: %w", quickID, err)
}
checked++
}
if checked == 0 {
return a.Allowed(subject, policyTY.ActionAction, policyTY.ResourceAction)
}
return nil
}

// keyResourceParam matches routes/action.go
const keyResourceParam = "resource"

// allowedActionOnIDs checks the action verb against each target id. Ids on these
// routes are storage ids, so they are resolved to business names first
// (node -> gatewayId.nodeId) to match the names used in policies.
func (a *API) allowedActionOnIDs(subject Subject, kind string, ids []string) error {
if len(ids) == 0 {
return a.Allowed(subject, policyTY.ActionAction, kind)
}
for _, id := range ids {
id = strings.TrimSpace(id)
if id == "" {
continue
}
name := id
if biz, err := a.ResolveBusinessName(kind, id); err == nil && biz != "" {
name = biz
}
if err := a.Allowed(subject, policyTY.ActionAction, FormatResource(kind, name)); err != nil {
return fmt.Errorf("action on %s: %w", FormatResource(kind, name), err)
}
}
return nil
}

// actionQuickIDsFromBody reads the POST /api/action payload and returns the
// quick ids it targets. The body is restored for the handler.
func (a *API) actionQuickIDsFromBody(r *http.Request) ([]string, error) {
if r.Body == nil {
return nil, nil
}
body, err := io.ReadAll(io.LimitReader(r.Body, maxActionBodyBytes+1))
if err != nil {
return nil, err
}
if len(body) > maxActionBodyBytes {
return nil, fmt.Errorf("action payload too large")
}
r.Body = io.NopCloser(bytes.NewReader(body))
if len(body) == 0 {
return nil, nil
}

actions := make([]webHandlerTY.ActionConfig, 0)
if err := json.Unmarshal(body, &actions); err != nil {
// malformed payload: nothing to target, handler will report the parse error.
// Fail closed here by requiring the generic action capability.
return nil, nil
}
quickIDs := make([]string, 0, len(actions))
for _, axn := range actions {
quickIDs = append(quickIDs, axn.Resource)
}
return quickIDs, nil
}
Loading