feat: own shared user API tokens
This commit is contained in:
114
api_tokens_test.go
Normal file
114
api_tokens_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-user-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-user-plugin/controllers"
|
||||
usermiddleware "git.golem15.com/golem15/sm-user-plugin/middleware"
|
||||
"git.golem15.com/golem15/sm-user-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
)
|
||||
|
||||
func TestSharedAPITokenAuthenticatesWithCurrentUserPermissions(t *testing.T) {
|
||||
app, db := sessionApp(t, "0s")
|
||||
if err := app.Config.Set("golem15.user.api_tokens.prefix", "tst_"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user := insertUser(t, db, "token-shared@example.test", "correct-horse")
|
||||
var groupID uint
|
||||
if err := db.Raw(`INSERT INTO user_groups (name, code, permissions, created_at, updated_at)
|
||||
VALUES ('API operators', 'api-operators', '{"jz.bm.access_materials":1,"jz.bm.access_courses":0}', NOW(), NOW()) RETURNING id`).Scan(&groupID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Exec("INSERT INTO users_groups (user_id, user_group_id) VALUES (?, ?)", user.ID, groupID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secret, token, err := classes.MintAPIToken("tst_", user.ID, "shared", []string{"read", "write"}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(token.TokenHash, secret) || !strings.HasPrefix(secret, "tst_") {
|
||||
t.Fatalf("secret/hash = %q/%q", secret, token.TokenHash)
|
||||
}
|
||||
if err := db.Create(token).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
guard := &usermiddleware.APITokenGuard{App: app, Prefix: "tst_", ClientIP: func(*http.Request) string { return "203.0.113.7" }}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/example", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+secret)
|
||||
principal, credential, err := guard.AuthenticateCredential(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if principal.ID != user.ID || !principal.PermissionGrants["jz.bm.access_materials"] || principal.PermissionGrants["jz.bm.access_courses"] {
|
||||
t.Fatalf("principal = %#v", principal)
|
||||
}
|
||||
got, ok := credential.(*models.APIToken)
|
||||
if !ok || got.ID != token.ID || got.LastUsedAt == nil || got.LastUsedIP == nil || *got.LastUsedIP != "203.0.113.7" {
|
||||
t.Fatalf("credential = %#v", credential)
|
||||
}
|
||||
|
||||
called := false
|
||||
final := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })
|
||||
ctx := bouncer.WithCredential(bouncer.WithUser(context.Background(), principal), got)
|
||||
scoped := usermiddleware.Scope("write")(usermiddleware.Permission("jz.bm.access_materials")(final))
|
||||
scoped.ServeHTTP(httptest.NewRecorder(), req.WithContext(ctx))
|
||||
if !called {
|
||||
t.Fatal("shared token did not pass its scope and current user permission")
|
||||
}
|
||||
denied := httptest.NewRecorder()
|
||||
usermiddleware.Permission("jz.bm.access_courses")(final).ServeHTTP(denied, req.WithContext(ctx))
|
||||
if denied.Code != http.StatusForbidden {
|
||||
t.Fatalf("permission denial = %d %s", denied.Code, denied.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMintAPITokenPreservesScopeOrderWhileDeduplicating(t *testing.T) {
|
||||
_, token, err := classes.MintAPIToken("usr_", 1, "ordered", []string{"write", "ai", "write", "read"}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := strings.Join(token.Scopes.Get(), ","); got != "write,ai,read" {
|
||||
t.Fatalf("scopes = %q, want write,ai,read", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPITokenManagementShowsSecretOnlyOnCreate(t *testing.T) {
|
||||
app, db := sessionApp(t, "0s")
|
||||
if err := app.Config.Set("golem15.user.api_tokens.prefix", "usr_"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user := insertUser(t, db, "token-manage@example.test", "correct-horse")
|
||||
principal := &bouncer.Principal{ID: user.ID}
|
||||
req := httptest.NewRequest(http.MethodPost, "/_user/api/v1/tokens", strings.NewReader(`{"name":"automation","scopes":["read"]}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(bouncer.WithUser(req.Context(), principal))
|
||||
created := httptest.NewRecorder()
|
||||
controllers.APITokenStore(app).ServeHTTP(created, req)
|
||||
if created.Code != http.StatusCreated {
|
||||
t.Fatalf("create = %d %s", created.Code, created.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Token string `json:"token"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
}
|
||||
if err := json.Unmarshal(created.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(body.Token, "usr_") || strings.Contains(created.Body.String(), classes.HashAPIToken(body.Token)) {
|
||||
t.Fatalf("create body = %s", created.Body.String())
|
||||
}
|
||||
listReq := httptest.NewRequest(http.MethodGet, "/_user/api/v1/tokens", nil)
|
||||
listReq = listReq.WithContext(bouncer.WithUser(listReq.Context(), principal))
|
||||
listed := httptest.NewRecorder()
|
||||
controllers.APITokenIndex(app).ServeHTTP(listed, listReq)
|
||||
if listed.Code != http.StatusOK || strings.Contains(listed.Body.String(), body.Token) || strings.Contains(listed.Body.String(), classes.HashAPIToken(body.Token)) {
|
||||
t.Fatalf("list = %d %s", listed.Code, listed.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user