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 .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ linters:
allow:
- $gostd
- github.com/google/uuid
- github.com/gopherium/framework/goncierge
- github.com/gopherium/framework/gonsole
- github.com/gopherium/framework/pluginkit
plugins-never-import-internal:
Expand Down
33 changes: 6 additions & 27 deletions cmd/alphone/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
package main

import (
"context"
"fmt"
"time"

"github.com/gopherium/framework/gonsole"
"github.com/gopherium/framework/goncierge"
accounts "github.com/gopherium/framework/gonsole/auth"
accountspg "github.com/gopherium/framework/gonsole/auth/postgres"

"github.com/gopherium/alphone/internal/role"
)
Expand All @@ -20,32 +19,12 @@ const recordTimeout = 5 * time.Second
const recordsLimit = 50

// accountConfig returns the configuration of the account commands and of the check on the acting account.
func accountConfig(registry *role.Registry) accounts.Config {
func accountConfig(registry *goncierge.Registry) accounts.Config {
return accounts.Config{
Roles: declaredRoles(registry),
Capability: string(role.ManageUsers),
Roles: accounts.DeclaredRoles(registry, role.ManageUsers),
Capability: role.ManageUsers,
RecordTimeout: recordTimeout,
RecordsLimit: recordsLimit,
}
}

// declaredRoles returns the role table of the account commands, the plugins registered first to declare theirs.
func declaredRoles(registry *role.Registry) func(context.Context, gonsole.Call) (accounts.Roles, error) {
return func(ctx context.Context, call gonsole.Call) (accounts.Roles, error) {
loaded, err := call.Plugins(ctx)
if err != nil {
return accounts.Roles{}, err
}
if loaded.Failed != nil {
return accounts.Roles{}, fmt.Errorf("register plugins: %w", loaded.Failed)
}
held := registry.Roles()
known := make([]string, 0, len(held))
capabilities := make(map[string][]string, len(held))
for _, name := range held {
known = append(known, name.String())
capabilities[name.String()] = registry.CapabilitiesOf(name)
}
return accounts.Roles{Known: known, Privileged: registry.Privileged(), Capabilities: capabilities}, nil
Stores: accountspg.Stores,
}
}
33 changes: 27 additions & 6 deletions cmd/alphone/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

"github.com/gopherium/framework/goncierge"
"github.com/gopherium/framework/gonsole"
accounts "github.com/gopherium/framework/gonsole/auth"
"github.com/gopherium/framework/gonsole/testkit"
Expand Down Expand Up @@ -63,7 +64,7 @@ func roleOf(t *testing.T, databaseURL, email string) string {
// stewardDeclaring registers one plugin declaring the steward role.
func stewardDeclaring(sdk.Deps) ([]sdk.Plugin, error) {
return []sdk.Plugin{rolePlugin{declared: []sdk.RoleDeclaration{
{Name: "steward", Capabilities: []string{string(role.ManageUsers), string(role.ManageWebhooks)}},
{Name: "steward", Capabilities: []string{role.ManageUsers, role.ManageWebhooks}},
}}}, nil
}

Expand Down Expand Up @@ -164,7 +165,7 @@ func TestAccountCommandsRefuseARoleAPluginCannotDeclare(t *testing.T) {
got := testkit.Run(t, programOver(role.NewRegistry(), getenv, refused), "",
"account:grant-role", "-role", "admin", "-as", "admin@example.com")

if got.Code != gonsole.ExitFailed || !strings.Contains(got.Stderr, role.ErrEmptyRole.Error()) {
if got.Code != gonsole.ExitFailed || !strings.Contains(got.Stderr, goncierge.ErrInvalidName.Error()) {
t.Errorf("account:grant-role = %d with stderr %q, want 1 and the refused declaration named", got.Code, got.Stderr)
}
}
Expand Down Expand Up @@ -319,14 +320,14 @@ func TestAnAdminCannotGiveARoleAboveItsOwn(t *testing.T) {
env := accountsDatabase(t)
above := func(sdk.Deps) ([]sdk.Plugin, error) {
return []sdk.Plugin{rolePlugin{declared: []sdk.RoleDeclaration{
{Name: "steward", Capabilities: []string{string(role.ManageUsers), "manage_reports"}},
{Name: "steward", Capabilities: []string{role.ManageUsers, "steward.manage_reports"}},
}}}, nil
}

got := testkit.Run(t, programOver(role.NewRegistry(), testkit.Getenv(env), above), "",
"account:role", "maria.perez@example.com", "steward", "-yes", "-as", "admin@example.com")

want := "alphone: the role steward carries manage_reports, which the account admin@example.com lacks\n"
want := "alphone: the role steward carries steward.manage_reports, which the account admin@example.com lacks\n"
if got.Code != gonsole.ExitFailed || got.Stdout != "" || got.Stderr != want {
t.Errorf("account:role -role steward = %d, stdout %q, stderr %q, want 1 and %q",
got.Code, got.Stdout, got.Stderr, want)
Expand Down Expand Up @@ -367,6 +368,26 @@ func TestAnActingAccountCannotChangeItsOwnRoleOrDisableItself(t *testing.T) {
}
}

func TestAccountEnableRefusesAnAccountHoldingNoRole(t *testing.T) {
t.Parallel()

env := accountsDatabase(t)
storeRoleless(t, env["ALPHONE_DATABASE_URL"], "none@example.com")

got := testkit.Run(t, bareProgram(env), "", "account:enable", "none@example.com", "-yes", "-as", "admin@example.com")

want := testkit.Result{
Code: gonsole.ExitFailed,
Stderr: "alphone: the account none@example.com holds no role, give it a declared role before enabling it\n",
}
if got != want {
t.Errorf("account:enable = %+v, want %+v", got, want)
}
if records := recordsOf(t, env); len(records) != 0 {
t.Errorf("records = %q, want none", records)
}
}

func TestPaddedRecordSettingsReadLikePlainOnes(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -416,7 +437,7 @@ func TestTheRoleTableCountsEveryRoleThatManagesUsersAsPrivileged(t *testing.T) {
Name: "roles",
Summary: "read the role table of the account commands",
Run: func(ctx context.Context, call gonsole.Call) (err error) {
held, err = declaredRoles(registry)(ctx, call)
held, err = accountConfig(registry).Roles(ctx, call)
return err
},
}},
Expand All @@ -428,7 +449,7 @@ func TestTheRoleTableCountsEveryRoleThatManagesUsersAsPrivileged(t *testing.T) {
if want := []string{role.Admin.String(), "steward"}; !slices.Equal(held.Privileged, want) {
t.Errorf("privileged roles = %v, want %v, every role carrying manage_users", held.Privileged, want)
}
want := []string{string(role.ManageUsers), string(role.ManageWebhooks)}
want := []string{role.ManageUsers, role.ManageWebhooks}
if !slices.Equal(held.Capabilities["steward"], want) {
t.Errorf("the steward role carries %v, want %v", held.Capabilities["steward"], want)
}
Expand Down
10 changes: 6 additions & 4 deletions cmd/alphone/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import (

"github.com/jackc/pgx/v5/pgxpool"

"github.com/gopherium/framework/goncierge"
"github.com/gopherium/framework/gonsole"
accounts "github.com/gopherium/framework/gonsole/auth"
accountspg "github.com/gopherium/framework/gonsole/auth/postgres"
"github.com/gopherium/framework/pluginkit"
authkitpg "github.com/gopherium/gouncer/authkit/postgres"

"github.com/gopherium/alphone/internal/contact"
"github.com/gopherium/alphone/internal/event"
"github.com/gopherium/alphone/internal/graphres"
"github.com/gopherium/alphone/internal/postgres"
"github.com/gopherium/alphone/internal/role"
"github.com/gopherium/alphone/internal/webhook"
"github.com/gopherium/alphone/sdk"
)
Expand All @@ -29,7 +29,7 @@ type composeConfig struct {
composeSettings
databaseURL string
getenv func(string) string
roles *role.Registry
roles *goncierge.Registry
logger *slog.Logger
}

Expand All @@ -47,6 +47,7 @@ type composed struct {
hub *event.Hub
events nudgingPublisher
mailer graphres.Mailer
roles *goncierge.Registry
registered []sdk.Plugin
failed error
}
Expand All @@ -70,6 +71,7 @@ func compose(ctx context.Context, cfg composeConfig, plugins func(sdk.Deps) ([]s
tenants: postgres.NewTenantStore(pool),
worker: webhook.NewWorker(webhooks, cfg.logger, guard),
hub: event.NewHub(),
roles: cfg.roles,
}
built.events = nudgingPublisher{
dispatcher: webhook.NewDispatcher(webhooks, cfg.logger), worker: built.worker, hub: built.hub,
Expand Down Expand Up @@ -117,7 +119,7 @@ func abandon(ctx context.Context, built composed, grace time.Duration) error {

// migrations returns the schema steps every database takes, in the order they apply.
func migrations() []gonsole.Step {
return []gonsole.Step{accounts.Migration(), accounts.RecordMigration(), {Name: "core", Run: postgres.Migrate}}
return []gonsole.Step{accountspg.Migration(), accountspg.RecordMigration(), {Name: "core", Run: postgres.Migrate}}
}

// migrate applies every schema step to the database at databaseURL.
Expand Down
3 changes: 2 additions & 1 deletion cmd/alphone/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/google/uuid"

"github.com/gopherium/framework/goncierge"
"github.com/gopherium/framework/gonsole/testkit"

"github.com/gopherium/alphone/internal/role"
Expand Down Expand Up @@ -306,7 +307,7 @@ func TestRunNamesTheRegistrationFailureBesideARefusedRole(t *testing.T) {

err := run(t.Context(), getenv, io.Discard, partlyRegisteredWithARefusedRole)

if !errors.Is(err, errRegistration) || !errors.Is(err, role.ErrEmptyRole) {
if !errors.Is(err, errRegistration) || !errors.Is(err, goncierge.ErrInvalidName) {
t.Errorf("run() error = %v, want the registration failure and the refused role both named", err)
}
}
2 changes: 1 addition & 1 deletion cmd/alphone/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func (s *operatorScenario) holdMember(email string) error {
// declareRoleBeyond adds a plugin declaring the role called name with a capability the role held lacks.
func (s *operatorScenario) declareRoleBeyond(name, held string) {
s.plugins = besideTheCompiledPlugins(rolePlugin{declared: []sdk.RoleDeclaration{
{Name: name, Capabilities: []string{"reach_beyond_" + held}},
{Name: name, Capabilities: []string{"steward.reach_beyond_" + held}},
}})
}

Expand Down
9 changes: 5 additions & 4 deletions cmd/alphone/loadplugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"
"time"

"github.com/gopherium/framework/goncierge"
"github.com/gopherium/framework/gonsole"
"github.com/gopherium/framework/gonsole/testkit"

Expand Down Expand Up @@ -150,12 +151,12 @@ func TestLoadPluginsStopsThePluginsWhenARoleIsRefused(t *testing.T) {
_, err := loadPlugins(role.NewRegistry(), func(sdk.Deps) ([]sdk.Plugin, error) {
return []sdk.Plugin{
stoppingPlugin{stopped: &stopped},
rolePlugin{declared: []sdk.RoleDeclaration{{Name: "", Capabilities: []string{"manage_reports"}}}},
rolePlugin{declared: []sdk.RoleDeclaration{{Name: "", Capabilities: []string{"steward.manage_reports"}}}},
}, nil
})(t.Context(), call)

if !errors.Is(err, role.ErrEmptyRole) {
t.Fatalf("loadPlugins() error = %v, want %v in its chain", err, role.ErrEmptyRole)
if !errors.Is(err, goncierge.ErrInvalidName) {
t.Fatalf("loadPlugins() error = %v, want %v in its chain", err, goncierge.ErrInvalidName)
}
stoppedUnderTheStopGrace(t, err, &stopped)
}
Expand Down Expand Up @@ -230,7 +231,7 @@ func TestLoadPluginsNamesTheRegistrationFailureBesideARefusedRole(t *testing.T)
_, err := loadPlugins(role.NewRegistry(), partlyRegisteredWithARefusedRole)(
t.Context(), describingCall(nil, io.Discard))

if !errors.Is(err, errRegistration) || !errors.Is(err, role.ErrEmptyRole) {
if !errors.Is(err, errRegistration) || !errors.Is(err, goncierge.ErrInvalidName) {
t.Errorf("loadPlugins() error = %v, want the registration failure and the refused role both named", err)
}
}
4 changes: 2 additions & 2 deletions cmd/alphone/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ import (

"github.com/google/uuid"

"github.com/gopherium/framework/goncierge"
"github.com/gopherium/framework/gonsole"
"github.com/gopherium/framework/pluginkit"

"github.com/gopherium/alphone/internal/contact"
"github.com/gopherium/alphone/internal/postgres"
"github.com/gopherium/alphone/internal/role"
"github.com/gopherium/alphone/sdk"
)

// loadPlugins returns the registration the command line runs the compiled plugins through, starting none.
func loadPlugins(
registry *role.Registry, plugins func(sdk.Deps) ([]sdk.Plugin, error),
registry *goncierge.Registry, plugins func(sdk.Deps) ([]sdk.Plugin, error),
) func(context.Context, gonsole.Call) (gonsole.Loaded, error) {
return func(ctx context.Context, call gonsole.Call) (gonsole.Loaded, error) {
grace, err := call.Env.Duration("SHUTDOWN_STOP_GRACE", servingDefaults.StopGrace)
Expand Down
25 changes: 19 additions & 6 deletions cmd/alphone/privileged_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,37 @@ import (
func TestTheAdminSeamCarriesThePrivilegedCover(t *testing.T) {
t.Parallel()

held := adminConfig(nil)
registry := role.NewRegistry()

if !slices.Equal(held.Privileged, role.Privileged()) {
held := adminConfig(composed{roles: registry})

if want := registry.HoldersOf(role.ManageUsers); !slices.Equal(held.Privileged, want) {
t.Errorf("Privileged = %v, want %v, an empty cover admits every role at the brick's guard",
held.Privileged, role.Privileged())
held.Privileged, want)
}
if len(held.Privileged) == 0 {
t.Error("the cover is empty, want the roles that administer accounts")
}
if held.Roles != registry {
t.Errorf("Roles = %v, want the registry handed in", held.Roles)
}
}

func TestTheLoginSeamCarriesThePrivilegedCover(t *testing.T) {
t.Parallel()

held := authConfig(nil)
registry := role.NewRegistry()

if !slices.Equal(held.Privileged, role.Privileged()) {
t.Errorf("Privileged = %v, want %v", held.Privileged, role.Privileged())
held := authConfig(composed{roles: registry})

if want := registry.HoldersOf(role.ManageUsers); !slices.Equal(held.Privileged, want) {
t.Errorf("Privileged = %v, want %v", held.Privileged, want)
}
if len(held.Privileged) == 0 {
t.Error("the cover is empty, want the roles that administer accounts")
}
if held.Capabilities != registry {
t.Errorf("Capabilities = %v, want the registry handed in", held.Capabilities)
}
if held.CookieName == "" {
t.Error("the login seam names no cookie, want the session cookie the server reads")
Expand Down
7 changes: 4 additions & 3 deletions cmd/alphone/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"slices"

"github.com/gopherium/framework/goncierge"
"github.com/gopherium/framework/gonsole"
accounts "github.com/gopherium/framework/gonsole/auth"

Expand All @@ -16,12 +17,12 @@ import (

// program returns the alphone command line over getenv and the compiled plugins.
func program(getenv func(string) string, plugins func(sdk.Deps) ([]sdk.Plugin, error)) gonsole.Program {
return programOver(role.Default, getenv, plugins)
return programOver(role.NewRegistry(), getenv, plugins)
}

// programOver returns the alphone command line declaring plugin roles into registry, serve into role.Default.
// programOver returns the alphone command line declaring plugin roles into registry, serve into a registry of its own.
func programOver(
registry *role.Registry, getenv func(string) string, plugins func(sdk.Deps) ([]sdk.Plugin, error),
registry *goncierge.Registry, getenv func(string) string, plugins func(sdk.Deps) ([]sdk.Plugin, error),
) gonsole.Program {
accountCommands := accountConfig(registry)
return gonsole.Program{
Expand Down
Loading
Loading