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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ TAG := $(shell git rev-list --tags --max-count=1)
VERSION := $(shell git describe --tags ${TAG})
.PHONY: build check fmt lint test test-race vet test-cover-html help install proto admin-app compose-up-dev
.DEFAULT_GOAL := build
PROTON_COMMIT := "795c70f359264a3c21a4c6412097f139dfc387e6"
PROTON_COMMIT := "ee05c27600bd7d2782da4fde0997f84aa69e7eeb"

admin-app:
@echo " > generating admin build"
Expand Down
76 changes: 76 additions & 0 deletions cmd/reconcile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"fmt"
"os"

"github.com/MakeNowJust/heredoc"
"github.com/raystack/frontier/internal/reconcile"
cli "github.com/spf13/cobra"
)

func ReconcileCommand(cliConfig *Config) *cli.Command {
var (
filePath string
dryRun bool
header string
)
cmd := &cli.Command{
Use: "reconcile",
Short: "Reconcile declarative platform configuration to a desired-state file",
Long: heredoc.Doc(`
Converge platform resources to a declarative YAML spec via the admin API.

Currently supports the PlatformUser kind (platform admins/members). The file
is the source of truth: entries present are ensured, entries absent are removed.
Authenticate as a superuser (e.g. the bootstrap service account) via --header.
`),
Example: heredoc.Doc(`
$ frontier reconcile -f platform-users.yaml --dry-run -H "Authorization:Basic <base64>"
$ frontier reconcile -f platform-users.yaml -H "Authorization:Basic <base64>"
`),
Annotations: map[string]string{
"group": "core",
"client": "true",
},
RunE: func(cmd *cli.Command, args []string) error {
data, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("read desired-state file: %w", err)
}
adminClient, err := createAdminClient(cliConfig.Host)
if err != nil {
return err
}
registry := map[string]reconcile.Reconciler{
reconcile.KindPlatformUser: reconcile.NewPlatformUserReconciler(adminClient, header),
}
reports, runErr := reconcile.Run(cmd.Context(), registry, data, dryRun)
for _, rep := range reports {
printReconcileReport(cmd, rep)
}
return runErr
},
}
cmd.Flags().StringVarP(&filePath, "file", "f", "", "Path to the desired-state YAML file")
cmd.MarkFlagRequired("file")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Print the plan without applying changes")
cmd.Flags().StringVarP(&header, "header", "H", "", "Header <key>:<value> for auth, e.g. 'Authorization:Basic <base64>'")
bindFlagsFromClientConfig(cmd)
return cmd
}

func printReconcileReport(cmd *cli.Command, rep reconcile.Report) {
if len(rep.Planned) == 0 {
cmd.Printf("%s: no changes\n", rep.Kind)
return
}
verb := "applied"
if rep.DryRun {
verb = "planned"
}
cmd.Printf("%s (%s %d):\n", rep.Kind, verb, len(rep.Planned))
for _, p := range rep.Planned {
cmd.Printf(" - %s\n", p)
}
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func New(cliConfig *Config) *cli.Command {
cmd.AddCommand(PermissionCommand(cliConfig))
cmd.AddCommand(PolicyCommand(cliConfig))
cmd.AddCommand(SeedCommand(cliConfig))
cmd.AddCommand(ReconcileCommand(cliConfig))
cmd.AddCommand(configCommand())
cmd.AddCommand(versionCommand())
cmd.AddCommand(PreferencesCommand(cliConfig))
Expand Down
13 changes: 8 additions & 5 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ func StartServer(logger *slog.Logger, cfg *config.Frontier) error {
if err = deps.BootstrapService.MigrateRoles(ctx); err != nil {
return err
}
// promote normal users to superusers
if err = deps.BootstrapService.MakeSuperUsers(ctx); err != nil {
// ensure the config-seeded bootstrap superuser service account (for automation/GitOps).
// all other platform-user management is handled out-of-band via the GitOps reconcile flow.
if err = deps.BootstrapService.EnsureBootstrapSuperUser(ctx); err != nil {
return err
}

Expand Down Expand Up @@ -395,7 +396,7 @@ func buildAPIDependencies(

svUserRepo := postgres.NewServiceUserRepository(dbc)
scUserCredRepo := postgres.NewServiceUserCredentialRepository(dbc)
serviceUserService := serviceuser.NewService(logger, svUserRepo, scUserCredRepo, relationService)
serviceUserService := serviceuser.NewService(logger, svUserRepo, scUserCredRepo, relationService, auditRecordRepository)

var mailDialer mailer.Dialer = mailer.NewMockDialer()
if cfg.App.Mailer.SMTPHost != "" && cfg.App.Mailer.SMTPHost != "smtp.example.com" {
Expand Down Expand Up @@ -432,7 +433,7 @@ func buildAPIDependencies(
// back here because role.Service depends on permission.Service
permissionService.SetRoleService(roleService)
policyService := policy.NewService(policyPGRepository, relationService, roleService)
userService := user.NewService(userRepository, relationService, sessionService)
userService := user.NewService(userRepository, relationService, sessionService, auditRecordRepository)
patValidator := userpat.NewValidator(logger, userPATRepo, cfg.App.PAT)
authnService := authenticate.NewService(logger, cfg.App.Authentication,
postgres.NewFlowRepository(logger, dbc), mailDialer, tokenService, sessionService, userService, serviceUserService, webAuthConfig, patValidator)
Expand Down Expand Up @@ -569,14 +570,16 @@ func buildAPIDependencies(
namespaceService,
roleService,
permissionService,
userService,
authzSchemaRepository,
relationService,
policyService,
svUserRepo,
cfg.App.PAT.DeniedPermissionsSet(),
planService,
planBlobRepository,
svUserRepo,
scUserCredRepo,
serviceUserService,
)

cascadeDeleter := deleter.NewCascadeDeleter(organizationService, projectService, resourceService,
Expand Down
18 changes: 12 additions & 6 deletions config/sample.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,18 @@ app:

# platform level administration
admin:
# Email list of users which needs to be converted as superusers
# if the user is already present in the system, it is promoted to su
# if not, a new account is created with provided email id and promoted to su.
# UUIDs/slugs of existing users can also be provided instead of email ids
# but in that case a new user will not be created.
users: []
# bootstrap seeds a superuser SERVICE ACCOUNT from config (a username/password-style
# client_id + client_secret) so automation like the GitOps reconcile flow has a
# guaranteed superuser identity without a chicken-and-egg. The account is ensured and
# promoted to superuser on every boot (idempotent); the secret is rotated if it changes
# here. Authenticate with: Authorization: Basic base64(client_id:client_secret).
# Leave client_id/client_secret empty to disable.
# client_id must be a UUID (it is the service-account credential id); generate one
# (e.g. uuidgen) and keep it stable. client_secret is your chosen password.
bootstrap:
client_id: ""
client_secret: ""
# title: "GitOps Bootstrap Superuser"
# smtp configuration for sending emails
mailer:
smtp_host: smtp.example.com
Expand Down
94 changes: 94 additions & 0 deletions core/serviceuser/mocks/audit_record_repository.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading