Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d5c2fc938 | ||
|
|
87fe84076f | ||
|
|
c558a2330e | ||
|
|
f00222cb8d | ||
|
|
d1560cc0fe | ||
|
|
c5f5873c2c | ||
|
|
cd6bca11a1 | ||
|
|
961ce21fef | ||
|
|
aefe77ed1b | ||
|
|
556203ee18 | ||
|
|
7b7e17aa55 | ||
|
|
9f6c5eea6d | ||
|
|
dde19e2d12 |
No files matched your search
@@ -2,6 +2,29 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.8.0] - 2026-10-07
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(management)* Search users by email in any case (#443)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/lestrrat-go/jwx/v4 to v4.5.0 (#427)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to 7543a96 (#421)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to 512690a (#423)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to f44f6e8 (#425)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to 03fd17c (#429)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to 1cfcdb1 (#431)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to 3680233 (#433)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to e0174e5 (#435)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to 23fe807 (#437)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to 1e93e00 (#439)
|
||||||
|
- *(deps)* Update golang:1.27 docker digest to 162be52 (#441)
|
||||||
|
|
||||||
## [0.7.8] - 2026-09-02
|
## [0.7.8] - 2026-09-02
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.27@sha256:192b74998e350966280a2cbffbb6c40064754f7ec005096aa64f04d7ece4467e AS build
|
FROM golang:1.27@sha256:162be5298a40ed317005c8339c6de4d10d3eef336d66dc8e9259b03ab9d3a6d2 AS build
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ func main() {
|
|||||||
|
|
||||||
// Management API endpoints
|
// Management API endpoints
|
||||||
mux.HandleFunc("GET /api/v2/users-by-email", managementHandler.GetUsersByEmail)
|
mux.HandleFunc("GET /api/v2/users-by-email", managementHandler.GetUsersByEmail)
|
||||||
|
mux.HandleFunc("GET /api/v2/users", managementHandler.SearchUsers)
|
||||||
mux.HandleFunc("POST /api/v2/users", managementHandler.CreateUser)
|
mux.HandleFunc("POST /api/v2/users", managementHandler.CreateUser)
|
||||||
mux.HandleFunc("PATCH /api/v2/users/", managementHandler.UpdateUser)
|
mux.HandleFunc("PATCH /api/v2/users/", managementHandler.UpdateUser)
|
||||||
mux.HandleFunc("POST /api/v2/tickets/password-change", managementHandler.PasswordChangeTicket)
|
mux.HandleFunc("POST /api/v2/tickets/password-change", managementHandler.PasswordChangeTicket)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ go 1.26.0
|
|||||||
require (
|
require (
|
||||||
github.com/alecthomas/kong v1.16.1
|
github.com/alecthomas/kong v1.16.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lestrrat-go/jwx/v4 v4.4.0
|
github.com/lestrrat-go/jwx/v4 v4.5.0
|
||||||
github.com/rs/cors v1.11.1
|
github.com/rs/cors v1.11.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,5 +13,5 @@ require (
|
|||||||
github.com/lestrrat-go/dsig v1.4.0 // indirect
|
github.com/lestrrat-go/dsig v1.4.0 // indirect
|
||||||
github.com/lestrrat-go/option/v3 v3.0.0-alpha1 // indirect
|
github.com/lestrrat-go/option/v3 v3.0.0-alpha1 // indirect
|
||||||
github.com/valyala/fastjson v1.6.10 // indirect
|
github.com/valyala/fastjson v1.6.10 // indirect
|
||||||
golang.org/x/crypto v0.55.0 // indirect
|
golang.org/x/crypto v0.56.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -10,17 +10,17 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
|
|||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/lestrrat-go/dsig v1.4.0 h1:g7LUjK8cT74A5DzBXJI5HzsJuLhoYN0Wzj4nuOMIrH8=
|
github.com/lestrrat-go/dsig v1.4.0 h1:g7LUjK8cT74A5DzBXJI5HzsJuLhoYN0Wzj4nuOMIrH8=
|
||||||
github.com/lestrrat-go/dsig v1.4.0/go.mod h1:I8Nddg/vN2cUl/h8N7SRRApLnNNeyZPIqLYpvpOtGGo=
|
github.com/lestrrat-go/dsig v1.4.0/go.mod h1:I8Nddg/vN2cUl/h8N7SRRApLnNNeyZPIqLYpvpOtGGo=
|
||||||
github.com/lestrrat-go/jwx/v4 v4.4.0 h1:CzoK8+u++WF7vVEmxx9fB8VaheeXWZ698F6HZbrl6SI=
|
github.com/lestrrat-go/jwx/v4 v4.5.0 h1:lgU8YcqaJo/iZllX4AvSaQTzT2XpWWMrDcPMe6+KzqI=
|
||||||
github.com/lestrrat-go/jwx/v4 v4.4.0/go.mod h1:65utsGK/iSrjgGfu6iqj/TAvSfia6SSXkRpjHcKcTyg=
|
github.com/lestrrat-go/jwx/v4 v4.5.0/go.mod h1:HpoztFC5HrNj9iKkZJ3XTQQgzCY2Zto/29n89QhTC10=
|
||||||
github.com/lestrrat-go/option/v3 v3.0.0-alpha1 h1:dvdzLwm/Ba5CJUF3jQP7w/iNYSLfy7yyh9XXNa1WjxI=
|
github.com/lestrrat-go/option/v3 v3.0.0-alpha1 h1:dvdzLwm/Ba5CJUF3jQP7w/iNYSLfy7yyh9XXNa1WjxI=
|
||||||
github.com/lestrrat-go/option/v3 v3.0.0-alpha1/go.mod h1:5KSg20dfsKkNJtjDmaQRLZVXuUrzuCCcz/gbDK0pfKk=
|
github.com/lestrrat-go/option/v3 v3.0.0-alpha1/go.mod h1:5KSg20dfsKkNJtjDmaQRLZVXuUrzuCCcz/gbDK0pfKk=
|
||||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.unbound.se/unboundsoftware/auth0mock/store"
|
"git.unbound.se/unboundsoftware/auth0mock/store"
|
||||||
@@ -152,3 +153,45 @@ func (h *ManagementHandler) PasswordChangeTicket(w http.ResponseWriter, r *http.
|
|||||||
"ticket": "https://some-url",
|
"ticket": "https://some-url",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// emailQuery matches the one Lucene query this mock answers: an email phrase,
|
||||||
|
// email:"...", with \" and \\ escaped inside it.
|
||||||
|
var emailQuery = regexp.MustCompile(`^email:"((?:[^"\\]|\\.)*)"$`)
|
||||||
|
|
||||||
|
// SearchUsers handles GET /api/v2/users. Like Auth0's user search it matches
|
||||||
|
// email in any letter case (users-by-email does not), and it answers in the
|
||||||
|
// shape the Go SDK asks for by default (include_totals=true). Only an email
|
||||||
|
// phrase is understood; any other query is refused rather than answered wrong.
|
||||||
|
func (h *ManagementHandler) SearchUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query().Get("q")
|
||||||
|
match := emailQuery.FindStringSubmatch(q)
|
||||||
|
if match == nil {
|
||||||
|
http.Error(w, "only q=email:\"...\" is supported", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email := strings.NewReplacer(`\"`, `"`, `\\`, `\`).Replace(match[1])
|
||||||
|
|
||||||
|
h.logger.Debug("searching users", "email", email)
|
||||||
|
|
||||||
|
users := []UserResponse{}
|
||||||
|
for _, user := range h.userStore.List() {
|
||||||
|
if strings.EqualFold(user.Email, email) {
|
||||||
|
users = append(users, UserResponse{
|
||||||
|
Email: user.Email,
|
||||||
|
GivenName: user.GivenName,
|
||||||
|
FamilyName: user.FamilyName,
|
||||||
|
UserID: fmt.Sprintf("auth0|%s", user.UserID),
|
||||||
|
Picture: user.Picture,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"start": 0,
|
||||||
|
"limit": len(users),
|
||||||
|
"length": len(users),
|
||||||
|
"total": len(users),
|
||||||
|
"users": users,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.unbound.se/unboundsoftware/auth0mock/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func searchUsers(t *testing.T, h *ManagementHandler, q string) (int, []UserResponse) {
|
||||||
|
t.Helper()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v2/users?search_engine=v3&include_totals=true&q="+url.QueryEscape(q), nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.SearchUsers(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return rec.Code, nil
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Users []UserResponse `json:"users"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if body.Total != len(body.Users) {
|
||||||
|
t.Fatalf("total %d, users %d", body.Total, len(body.Users))
|
||||||
|
}
|
||||||
|
return rec.Code, body.Users
|
||||||
|
}
|
||||||
|
|
||||||
|
// The search matches email in any letter case, as Auth0's does: a backend that
|
||||||
|
// checks whether an address is taken must find it however it was typed.
|
||||||
|
func TestSearchUsersMatchesEmailInAnyCase(t *testing.T) {
|
||||||
|
users := store.NewUserStore()
|
||||||
|
users.Create("anna@kpmg.se", &store.User{GivenName: "Anna"})
|
||||||
|
users.Create("bob@acme.se", &store.User{GivenName: "Bob"})
|
||||||
|
h := NewManagementHandler(users, slog.New(slog.DiscardHandler))
|
||||||
|
|
||||||
|
for _, q := range []string{`email:"anna@kpmg.se"`, `email:"ANNA@KPMG.SE"`, `email:"Anna@Kpmg.se"`} {
|
||||||
|
code, found := searchUsers(t, h, q)
|
||||||
|
if code != http.StatusOK || len(found) != 1 || found[0].UserID != "auth0|anna@kpmg.se" {
|
||||||
|
t.Fatalf("%s: code %d, found %+v", q, code, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := searchUsers(t, h, `email:"nobody@acme.se"`); len(found) != 0 {
|
||||||
|
t.Fatalf("an unknown address found %+v", found)
|
||||||
|
}
|
||||||
|
// A quoted * is part of the address, not a wildcard.
|
||||||
|
if _, found := searchUsers(t, h, `email:"*@kpmg.se"`); len(found) != 0 {
|
||||||
|
t.Fatalf("a quoted * matched %+v", found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchUsersRefusesOtherQueries(t *testing.T) {
|
||||||
|
h := NewManagementHandler(store.NewUserStore(), slog.New(slog.DiscardHandler))
|
||||||
|
for _, q := range []string{"", "name:anna", `email:"a@b.se" OR name:x`} {
|
||||||
|
if code, _ := searchUsers(t, h, q); code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("%q: code %d, want 400", q, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user