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()) } }