-
Notifications
You must be signed in to change notification settings - Fork 33
Absorb the roles bricks and ask capabilities #82
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
Merged
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
fbd06b6
feat(role): let plugins declare the roles a deployment knows
SirLouen b71dff2
chore(deps): absorb the gouncer releases that name roles
SirLouen 13a0aad
feat(cmd): take the role createadmin starts an account under
SirLouen f3de4b4
feat(cmd): hand the brick the roles that administer accounts
SirLouen a013c98
feat(cmd): seed each demo account under the role it holds
SirLouen 4019c70
test(cmd): name the role every bootstrap invocation passes
SirLouen 54bcff2
feat(graphres): create an account under the narrowest role
SirLouen 556250e
test: build the brick's admin config in every harness
SirLouen 1f32e0f
feat(postgres): move every tier onto the account and drop the table
SirLouen 7413067
refactor(server): read the role the identity carries
SirLouen 2c9f4bb
feat(graphres): guard every role write behind the caller's reach
SirLouen f051697
feat(cmd): give a role to every account holding none
SirLouen a1fc02c
refactor(cmd): name the seams that carry the privileged cover
SirLouen f9cd441
test: drive the role rails through the brick
SirLouen 25bb1d4
docs: restore the role column rather than the dropped table
SirLouen 23f39c1
feat(role): add ErrBeyondReach for role capability violations
SirLouen d8434c4
feat(role): name every capability the registry holds
SirLouen 6cdead1
feat(graph): let a root field name the capability it needs
SirLouen 14a2695
feat(graphres): gate every root field on a capability
SirLouen 6bd5186
test(graph): refuse a bare admin flag and an unknown capability
SirLouen c21be67
feat(graph): answer what the caller may do and may grant
SirLouen c1a394f
feat(sdk): ask a capability rather than compare a role
SirLouen 97e7393
feat(frontend): offer only the roles the reader may grant
SirLouen 44932ee
test(e2e): prove the role control in a real browser
SirLouen d7904f8
docs: teach plugin authors to ask a capability
SirLouen 8e56a84
feat(graph): answer what the caller may do and may grant
SirLouen 9b8f5b1
fix(graphres): speak the brick refusals in the deployment's own voice
SirLouen e12dec8
docs: describe the capability model the graph now answers
SirLouen 659e5c4
fix(postgres): keep every role when the move rolls back
SirLouen 5a99060
fix(cmd): let a command name a role a plugin declared
SirLouen c4903f7
feat(graphres): name the capability a refused caller lacked
SirLouen 2cdc030
test: pin the spoken refusals and the privileged boundary
SirLouen ad2e5da
fix(frontend): refuse a role change while one is in flight
SirLouen c9e8d6f
docs(sdk): say what a role provider returns
SirLouen 13476ed
docs: name the capability extension and restore every role
SirLouen bf61d95
fix(graphres): read a null capability as none declared
SirLouen 448174b
fix(cmd): stop the plugin host under its own bounded context
SirLouen c075052
docs: stream the roles through psql rather than into the container
SirLouen 342ae3c
docs: say an account may hold no role at all
SirLouen 931453b
test(graph): let a plugin name the capabilities it declares
SirLouen 86af0e0
docs: state the role rules without promising an invariant
SirLouen 65fd218
docs: stop the role export on the first error
SirLouen 9f98ab8
test(graphres): pin what an account holding no role may reach
SirLouen f71d6d6
test(graph): recognise a plugin path whichever separator globbed it
SirLouen d00c7b3
docs: correct what an account holding no role can still do
SirLouen f8c8c1e
docs: keep a failed role export from replacing a good file
SirLouen 38d34eb
docs: hyphenate the plugin-declared role compound
SirLouen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // SPDX-License-Identifier: Elastic-2.0 | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "flag" | ||
| "fmt" | ||
| "io" | ||
|
|
||
| authkitpg "github.com/gopherium/gouncer/authkit/postgres" | ||
|
|
||
| "github.com/gopherium/alphone/internal/role" | ||
| ) | ||
|
|
||
| // grantRole gives a role to every account holding none, from command-line arguments. | ||
| func grantRole(ctx context.Context, getenv func(string) string, args []string, stdout io.Writer) error { | ||
| flags := flag.NewFlagSet("grantrole", flag.ContinueOnError) | ||
| flags.SetOutput(stdout) | ||
| named := flags.String("role", "", "role to give every account holding none") | ||
| if err := flags.Parse(args); err != nil { | ||
| if errors.Is(err, flag.ErrHelp) { | ||
| return nil | ||
| } | ||
| return fmt.Errorf("parse flags: %w", err) | ||
| } | ||
|
|
||
| databaseURL := getenv("ALPHONE_DATABASE_URL") | ||
| if databaseURL == "" { | ||
| return errors.New("ALPHONE_DATABASE_URL is required") | ||
| } | ||
| held, err := role.Parse(*named) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if err := migrateSchemas(ctx, databaseURL); err != nil { | ||
| return err | ||
| } | ||
| return authkitpg.RunGrantRole(ctx, databaseURL, []string{"-role", held.String()}, stdout) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // SPDX-License-Identifier: Elastic-2.0 | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "os/exec" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| authkitpg "github.com/gopherium/gouncer/authkit/postgres" | ||
|
|
||
| "github.com/gopherium/alphone/internal/role" | ||
| ) | ||
|
|
||
| func TestMainBinaryGrantsARoleToEveryAccountHoldingNone(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| binary, env := coverBinary(t) | ||
| databaseURL := testDatabaseURL(t) | ||
| holding := storeRoleless(t, databaseURL, "none@example.com") | ||
| var stdout bytes.Buffer | ||
| granting := exec.Command(binary, "grantrole", "-role", "member") | ||
| granting.Dir = t.TempDir() | ||
| granting.Env = append(env, "ALPHONE_DATABASE_URL="+databaseURL) | ||
| granting.Stdout = &stdout | ||
|
|
||
| if err := granting.Run(); err != nil { | ||
| t.Fatalf("grantrole: %v, answered %s", err, stdout.String()) | ||
| } | ||
|
|
||
| users := authkitpg.NewUserStore(testPool(t, databaseURL)) | ||
| held, err := users.UserByID(t.Context(), holding.ID) | ||
| if err != nil { | ||
| t.Fatalf("UserByID() error = %v, want nil", err) | ||
| } | ||
| if held.Role != role.Member.String() { | ||
| t.Errorf("role = %q, want %q written by the running binary", held.Role, role.Member.String()) | ||
| } | ||
| if !strings.Contains(stdout.String(), "1") { | ||
| t.Errorf("output = %q, want it to count the account that took the role", stdout.String()) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| // SPDX-License-Identifier: Elastic-2.0 | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "errors" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/gopherium/gouncer" | ||
| authkitpg "github.com/gopherium/gouncer/authkit/postgres" | ||
|
|
||
| "github.com/gopherium/alphone/internal/role" | ||
| ) | ||
|
|
||
| // storeRoleless stores one account holding no role and returns it. | ||
| func storeRoleless(t *testing.T, databaseURL, email string) gouncer.User { | ||
| t.Helper() | ||
| held, err := gouncer.NewUser(email, "Maria Perez", "correct horse battery") | ||
| if err != nil { | ||
| t.Fatalf("gouncer.NewUser() error = %v, want nil", err) | ||
| } | ||
| if err := authkitpg.NewUserStore(testPool(t, databaseURL)).CreateUser(t.Context(), held); err != nil { | ||
| t.Fatalf("CreateUser() error = %v, want nil", err) | ||
| } | ||
| return held | ||
| } | ||
|
|
||
| func TestGrantRoleReachesEveryAccountHoldingNone(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| databaseURL := testDatabaseURL(t) | ||
| getenv := testGetenv(map[string]string{"ALPHONE_DATABASE_URL": databaseURL}) | ||
| holding := storeRoleless(t, databaseURL, "none@example.com") | ||
| var stdout strings.Builder | ||
|
|
||
| if err := grantRole(t.Context(), getenv, []string{"-role", "member"}, &stdout); err != nil { | ||
| t.Fatalf("grantRole() error = %v, want nil", err) | ||
| } | ||
|
|
||
| users := authkitpg.NewUserStore(testPool(t, databaseURL)) | ||
| held, err := users.UserByID(t.Context(), holding.ID) | ||
| if err != nil { | ||
| t.Fatalf("UserByID() error = %v, want nil", err) | ||
| } | ||
| if held.Role != role.Member.String() { | ||
| t.Errorf("role = %q, want %q", held.Role, role.Member.String()) | ||
| } | ||
| if !strings.Contains(stdout.String(), "1") { | ||
| t.Errorf("output = %q, want it to count the account that took the role", stdout.String()) | ||
| } | ||
| } | ||
|
|
||
| func TestGrantRoleLeavesAnAccountThatHoldsOne(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| databaseURL := testDatabaseURL(t) | ||
| getenv := testGetenv(map[string]string{"ALPHONE_DATABASE_URL": databaseURL}) | ||
| standing := storeRoleless(t, databaseURL, "standing@example.com") | ||
| if err := grantRole(t.Context(), getenv, []string{"-role", "admin"}, &strings.Builder{}); err != nil { | ||
| t.Fatalf("first grantRole() error = %v, want nil", err) | ||
| } | ||
|
|
||
| if err := grantRole(t.Context(), getenv, []string{"-role", "member"}, &strings.Builder{}); err != nil { | ||
| t.Fatalf("second grantRole() error = %v, want nil", err) | ||
| } | ||
|
|
||
| users := authkitpg.NewUserStore(testPool(t, databaseURL)) | ||
| held, err := users.UserByID(t.Context(), standing.ID) | ||
| if err != nil { | ||
| t.Fatalf("UserByID() error = %v, want nil", err) | ||
| } | ||
| if held.Role != role.Admin.String() { | ||
| t.Errorf("role = %q, want %q, a second run leaves an account that holds one", held.Role, role.Admin.String()) | ||
| } | ||
| } | ||
|
|
||
| func TestGrantRoleRefusesARoleTheRegistryDoesNotKnow(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| getenv := testGetenv(map[string]string{"ALPHONE_DATABASE_URL": testDatabaseURL(t)}) | ||
|
|
||
| err := grantRole(t.Context(), getenv, []string{"-role", "superadmin"}, &strings.Builder{}) | ||
|
|
||
| if !errors.Is(err, role.ErrUnknownTier) { | ||
| t.Errorf("grantRole() error = %v, want a role no plugin declared refused", err) | ||
| } | ||
| } | ||
|
|
||
| func TestGrantRoleNamesTheMissingDatabaseBeforeTheRole(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| err := grantRole(t.Context(), testGetenv(nil), nil, &strings.Builder{}) | ||
|
|
||
| if err == nil || errors.Is(err, role.ErrUnknownTier) { | ||
| t.Errorf("grantRole() error = %v, want the database url named first", err) | ||
| } | ||
| } | ||
|
|
||
| func TestGrantRoleRefusesAFlagItDoesNotKnow(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| err := grantRole(t.Context(), testGetenv(nil), []string{"-bogus"}, &strings.Builder{}) | ||
|
|
||
| if err == nil { | ||
| t.Error("grantRole() error = nil, want the unknown flag refused") | ||
| } | ||
| } | ||
|
|
||
| func TestGrantRoleReportsADatabaseItCannotReach(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| getenv := testGetenv(map[string]string{"ALPHONE_DATABASE_URL": unreachableDatabaseURL}) | ||
|
|
||
| err := grantRole(t.Context(), getenv, []string{"-role", "member"}, &strings.Builder{}) | ||
|
|
||
| if err == nil { | ||
| t.Error("grantRole() error = nil, want the unreachable database reported") | ||
| } | ||
| } | ||
|
|
||
| func TestGrantRolePrintsItsFlags(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| var stdout strings.Builder | ||
|
|
||
| err := grantRole(t.Context(), testGetenv(nil), []string{"-h"}, &stdout) | ||
|
|
||
| if err != nil { | ||
| t.Fatalf("grantRole() error = %v, want nil", err) | ||
| } | ||
| if !strings.Contains(stdout.String(), "-role") { | ||
| t.Errorf("output = %q, want the flags listed", stdout.String()) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add canonical Go doc comments to each new test function.
cmd/alphone/createadmin_test.go#L122-L122: Add a comment forTestCreateAdminRefusesARoleTheRegistryDoesNotKnow.cmd/alphone/createadmin_test.go#L140-L140: Add a comment forTestCreateAdminNamesTheMissingDatabaseBeforeTheRole.cmd/alphone/grantrole_exec_test.go#L16-L16: Add a comment forTestMainBinaryGrantsARoleToEveryAccountHoldingNone.cmd/alphone/grantrole_test.go#L29-L29: Add a comment forTestGrantRoleReachesEveryAccountHoldingNone.cmd/alphone/grantrole_test.go#L54-L54: Add a comment forTestGrantRoleLeavesAnAccountThatHoldsOne.cmd/alphone/grantrole_test.go#L78-L78: Add a comment forTestGrantRoleRefusesARoleTheRegistryDoesNotKnow.cmd/alphone/grantrole_test.go#L90-L90: Add a comment forTestGrantRoleNamesTheMissingDatabaseBeforeTheRole.cmd/alphone/grantrole_test.go#L100-L100: Add a comment forTestGrantRoleRefusesAFlagItDoesNotKnow.cmd/alphone/grantrole_test.go#L110-L110: Add a comment forTestGrantRoleReportsADatabaseItCannotReach.cmd/alphone/grantrole_test.go#L122-L122: Add a comment forTestGrantRolePrintsItsFlags.As per coding guidelines,
**/*.{go,ts,tsx}requires every function to carry a canonical Go doc comment.📍 Affects 3 files
cmd/alphone/createadmin_test.go#L122-L122(this comment)cmd/alphone/createadmin_test.go#L140-L140cmd/alphone/grantrole_exec_test.go#L16-L16cmd/alphone/grantrole_test.go#L29-L29cmd/alphone/grantrole_test.go#L54-L54cmd/alphone/grantrole_test.go#L78-L78cmd/alphone/grantrole_test.go#L90-L90cmd/alphone/grantrole_test.go#L100-L100cmd/alphone/grantrole_test.go#L110-L110cmd/alphone/grantrole_test.go#L122-L122🤖 Prompt for AI Agents
Source: Coding guidelines