chore: mercury changes

This commit is contained in:
xuu
2024-04-05 12:40:51 -06:00
parent d4e021386b
commit 1f8b4ab24f
19 changed files with 895 additions and 298 deletions

View File

@@ -2,6 +2,7 @@ package ident
import (
"context"
"errors"
"fmt"
"io"
"net/http"
@@ -52,11 +53,11 @@ func FromContext(ctx context.Context) Ident {
type IDM struct {
rand io.Reader
sources []source
pwd *passwd.Passwd
pwd *passwd.Passwd
}
func NewIDM(pwd *passwd.Passwd, rand io.Reader) *IDM {
return &IDM{pwd: pwd, rand:rand}
return &IDM{pwd: pwd, rand: rand}
}
func (idm *IDM) Add(p int, h Handler) {
@@ -70,18 +71,17 @@ func (idm *IDM) Passwd(pass, hash []byte) ([]byte, error) {
// ReadIdent read ident from a list of ident handlers
func (idm *IDM) ReadIdent(r *http.Request) (Ident, error) {
var errs error
for _, source := range idm.sources {
u, err := source.ReadIdent(r)
if err != nil {
return Anonymous, err
}
errs = errors.Join(errs, err)
if u.Session().Active {
return u, err
if u != nil && u.Session().Active {
return u, errs
}
}
return Anonymous, nil
return Anonymous, errs
}
func (idm *IDM) RegisterIdent(ctx context.Context, identity, displayName string, passwd []byte) (Ident, error) {

View File

@@ -2,16 +2,10 @@ package ident
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/oklog/ulid/v2"
"go.sour.is/passwd"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/locker"
)
var (
@@ -24,7 +18,7 @@ var (
nick = `value="` + nick + `"`
}
return `
<form id="login" hx-post="ident/login" hx-target="#login" hx-swap="outerHTML">
<form id="login" hx-post="ident/session" hx-target="#login" hx-swap="outerHTML">
<input required id="login-identity" name="identity" type="text" ` + nick + `placeholder="Identity..." />
<input required id="login-passwd" name="passwd" type="password" ` + indicator + ` placeholder="Password..." />
@@ -32,8 +26,12 @@ var (
<button hx-get="ident/register">Register</button>
</form>`
}
logoutForm = func(display string) string {
return `<button id="login" hx-post="ident/logout" hx-target="#login" hx-swap="outerHTML">` + display + ` (logout)</button>`
logoutForm = func(id Ident) string {
display := id.Identity()
if id, ok := id.(interface{ DisplayName() string }); ok {
display = id.DisplayName()
}
return `<button id="login" hx-delete="ident/session" hx-target="#login" hx-swap="outerHTML">` + display + ` (logout)</button>`
}
registerForm = `
<form id="login" hx-post="ident/register" hx-target="#login" hx-swap="outerHTML">
@@ -46,152 +44,104 @@ var (
</form>`
)
type sessions map[ulid.ULID]Ident
type root struct {
idm *IDM
sessions *locker.Locked[sessions]
type sessionIF interface {
ReadIdent(r *http.Request) (Ident, error)
CreateSession(context.Context, http.ResponseWriter, Ident) error
DestroySession(context.Context, http.ResponseWriter, Ident) error
}
func NewHTTP(idm *IDM) *root {
sessions := make(sessions)
type root struct {
idm *IDM
session sessionIF
}
func NewHTTP(idm *IDM, session sessionIF) *root {
idm.Add(0, session)
return &root{
idm: idm,
sessions: locker.New(sessions),
idm: idm,
session: session,
}
}
func (s *root) RegisterHTTP(mux *http.ServeMux) {
mux.HandleFunc("/ident", s.get)
mux.HandleFunc("/ident/register", s.register)
mux.HandleFunc("/ident/login", s.login)
mux.HandleFunc("/ident/logout", s.logout)
mux.HandleFunc("/ident", s.sessionHTTP)
mux.HandleFunc("/ident/register", s.registerHTTP)
mux.HandleFunc("/ident/session", s.sessionHTTP)
}
func (s *root) RegisterAPIv1(mux *http.ServeMux) {
mux.HandleFunc("GET /ident", s.sessionV1)
mux.HandleFunc("POST /ident", s.registerV1)
mux.HandleFunc("POST /ident/session", s.loginV1)
mux.HandleFunc("DELETE /ident/session", s.logoutV1)
mux.HandleFunc("GET /ident", s.getV1)
mux.HandleFunc("/ident/session", s.sessionV1)
}
func (s *root) RegisterMiddleware(hdlr http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
r = r.WithContext(ctx)
cookie, err := r.Cookie("sour.is-ident")
id, err := s.idm.ReadIdent(r)
span.RecordError(err)
if err != nil {
hdlr.ServeHTTP(w, r)
return
if id == nil {
id = Anonymous
}
sessionID, err := ulid.Parse(cookie.Value)
span.RecordError(err)
var id Ident = Anonymous
if err == nil {
err = s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
if session, ok := sessions[sessionID]; ok {
id = session
}
return nil
})
}
span.RecordError(err)
r = r.WithContext(context.WithValue(r.Context(), contextKey, id))
hdlr.ServeHTTP(w, r)
})
}
func (s *root) createSession(ctx context.Context, id Ident) error {
return s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
sessions[id.Session().SessionID] = id
return nil
})
}
func (s *root) destroySession(ctx context.Context, id Ident) error {
session := id.Session()
session.Active = false
return s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
delete(sessions, session.SessionID)
return nil
})
}
func (s *root) getV1(w http.ResponseWriter, r *http.Request) {
func (s *root) sessionV1(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
var id Ident = FromContext(ctx)
if id == nil {
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
switch r.Method {
case http.MethodGet:
if id == nil {
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
fmt.Fprint(w, id)
case http.MethodPost:
if !id.Session().Active {
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
err := s.session.CreateSession(ctx, w, id)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
fmt.Fprint(w, id)
case http.MethodDelete:
if !id.Session().Active {
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
err := s.session.DestroySession(ctx, w, FromContext(ctx))
if err != nil {
span.RecordError(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
http.Error(w, "GONE", http.StatusGone)
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
fmt.Fprint(w, id)
}
func (s *root) loginV1(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
id, err := s.idm.ReadIdent(r)
span.RecordError(err)
if err != nil {
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
if !id.Session().Active {
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
err = s.createSession(ctx, id)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "sour.is-ident",
Value: id.Session().SessionID.String(),
Expires: time.Time{},
Path: "/",
Secure: false,
HttpOnly: true,
})
fmt.Fprint(w, id)
}
func (s *root) logoutV1(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
if r.Method != http.MethodPost {
http.Error(w, "ERR", http.StatusMethodNotAllowed)
return
}
err := s.destroySession(ctx, FromContext(ctx))
if err != nil {
span.RecordError(err)
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
http.SetCookie(w, &http.Cookie{Name: "sour.is-ident", MaxAge: -1})
http.Error(w, "GONE", http.StatusGone)
}
func (s *root) registerV1(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
if r.Method != http.MethodPost {
http.Error(w, "ERR", http.StatusMethodNotAllowed)
return
}
r.ParseForm()
identity := r.Form.Get("identity")
@@ -211,107 +161,60 @@ func (s *root) registerV1(w http.ResponseWriter, r *http.Request) {
http.Error(w, "OK "+identity, http.StatusCreated)
}
func (s *root) get(w http.ResponseWriter, r *http.Request) {
func (s *root) sessionHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
var id Ident = FromContext(ctx)
if id == nil {
http.Error(w, loginForm("", true), http.StatusOK)
return
}
id := FromContext(ctx)
if !id.Session().Active {
http.Error(w, loginForm("", true), http.StatusOK)
return
}
display := id.Identity()
if id, ok := id.(interface{ DisplayName() string }); ok {
display = id.DisplayName()
}
fmt.Fprint(w, logoutForm(display))
}
func (s *root) login(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
if r.Method == http.MethodGet {
switch r.Method {
case http.MethodGet:
if id.Session().Active {
fmt.Fprint(w, logoutForm(id))
return
}
fmt.Fprint(w, loginForm("", true))
return
}
id, err := s.idm.ReadIdent(r)
span.RecordError(err)
if err != nil {
if errors.Is(err, passwd.ErrNoMatch) {
case http.MethodPost:
if !id.Session().Active {
http.Error(w, loginForm("", false), http.StatusOK)
return
}
http.Error(w, "ERROR", http.StatusInternalServerError)
return
err := s.session.CreateSession(ctx, w, id)
span.RecordError(err)
if err != nil {
http.Error(w, "ERROR", http.StatusInternalServerError)
return
}
fmt.Fprint(w, logoutForm(id))
case http.MethodDelete:
err := s.session.DestroySession(ctx, w, FromContext(ctx))
span.RecordError(err)
if err != nil {
http.Error(w, loginForm("", true), http.StatusUnauthorized)
return
}
fmt.Fprint(w, loginForm("", true))
default:
http.Error(w, "ERROR", http.StatusMethodNotAllowed)
}
if !id.Session().Active {
http.Error(w, loginForm("", false), http.StatusOK)
return
}
err = s.createSession(ctx, id)
span.RecordError(err)
if err != nil {
http.Error(w, "ERROR", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "sour.is-ident",
Value: id.Session().SessionID.String(),
Expires: time.Time{},
Path: "/",
Secure: false,
HttpOnly: true,
})
display := id.Identity()
if id, ok := id.(interface{ DisplayName() string }); ok {
display = id.DisplayName()
}
fmt.Fprint(w, logoutForm(display))
}
func (s *root) logout(w http.ResponseWriter, r *http.Request) {
func (s *root) registerHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
if r.Method != http.MethodPost {
http.Error(w, "ERR", http.StatusMethodNotAllowed)
return
}
http.SetCookie(w, &http.Cookie{Name: "sour.is-ident", MaxAge: -1})
err := s.destroySession(ctx, FromContext(ctx))
span.RecordError(err)
if err != nil {
http.Error(w, loginForm("", true), http.StatusUnauthorized)
return
}
fmt.Fprint(w, loginForm("", true))
}
func (s *root) register(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
if r.Method == http.MethodGet {
switch r.Method {
case http.MethodGet:
fmt.Fprint(w, registerForm)
return
}
if r.Method != http.MethodPost {
case http.MethodPost:
// break
default:
http.Error(w, "ERR", http.StatusMethodNotAllowed)
return
}
r.ParseForm()
@@ -335,26 +238,12 @@ func (s *root) register(w http.ResponseWriter, r *http.Request) {
return
}
err = s.createSession(ctx, id)
err = s.session.CreateSession(ctx, w, id)
span.RecordError(err)
if err != nil {
http.Error(w, "ERROR", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "sour.is-ident",
Value: id.Session().SessionID.String(),
Expires: time.Time{},
Path: "/",
Secure: false,
HttpOnly: true,
})
display = id.Identity()
if id, ok := id.(interface{ DisplayName() string }); ok {
display = id.DisplayName()
}
http.Error(w, logoutForm(display), http.StatusCreated)
http.Error(w, logoutForm(id), http.StatusCreated)
}

View File

@@ -12,6 +12,9 @@ import (
"go.sour.is/pkg/ident"
)
const identNS = "ident."
const identSFX = ".credentials"
type registry interface {
GetIndex(ctx context.Context, match, search string) (c mercury.Config, err error)
GetConfig(ctx context.Context, match, search, fields string) (mercury.Config, error)
@@ -22,12 +25,13 @@ type mercuryIdent struct {
identity string
display string
passwd []byte
ed25519 []byte
ident.SessionInfo
}
func (id *mercuryIdent) Identity() string { return id.identity }
func (id *mercuryIdent) DisplayName() string { return id.display }
func (id *mercuryIdent) Space() string { return "mercury.@" + id.identity }
func (id *mercuryIdent) Space() string { return identNS + "@" + id.identity }
func (id *mercuryIdent) FromConfig(cfg mercury.Config) error {
if id == nil {
@@ -35,7 +39,7 @@ func (id *mercuryIdent) FromConfig(cfg mercury.Config) error {
}
for _, s := range cfg {
if !strings.HasPrefix(s.Space, "mercury.") {
if !strings.HasPrefix(s.Space, identNS) {
continue
}
if id.identity == "" {
@@ -44,7 +48,7 @@ func (id *mercuryIdent) FromConfig(cfg mercury.Config) error {
}
switch {
case strings.HasSuffix(s.Space, ".ident"):
case strings.HasSuffix(s.Space, ".credentials"):
id.passwd = []byte(s.FirstValue("passwd").First())
default:
id.display = s.FirstValue("displayName").First()
@@ -74,10 +78,10 @@ func (id *mercuryIdent) ToConfig() mercury.Config {
},
},
&mercury.Space{
Space: space + ".ident",
Space: space + identSFX,
List: []mercury.Value{
{
Space: space + ".ident",
Space: space + identSFX,
Seq: 1,
Name: "passwd",
Values: []string{string(id.passwd)},
@@ -105,20 +109,38 @@ func NewMercury(r registry, pwd *ident.IDM) *mercurySource {
}
func (s *mercurySource) ReadIdent(r *http.Request) (ident.Ident, error) {
if id, err := s.readIdentBasic(r); id != nil {
return id, err
}
if id, err := s.readIdentURL(r); id != nil {
return id, err
}
if id, err := s.readIdentHTTP(r); id != nil {
return id, err
}
return nil, fmt.Errorf("no auth")
}
func (s *mercurySource) readIdentURL(r *http.Request) (ident.Ident, error) {
ctx, span := lg.Span(r.Context())
defer span.End()
if r.Method != http.MethodPost {
return nil, fmt.Errorf("method not allowed")
pass, ok := r.URL.User.Password()
if !ok {
return nil, nil
}
r.ParseForm()
id := &mercuryIdent{
identity: r.Form.Get("identity"),
passwd: []byte(r.Form.Get("passwd")),
identity: r.URL.User.Username(),
passwd: []byte(pass),
}
space := id.Space()
c, err := s.r.GetConfig(ctx, "trace:"+space+".ident", "", "")
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
if err != nil {
span.RecordError(err)
return id, err
@@ -144,6 +166,95 @@ func (s *mercurySource) ReadIdent(r *http.Request) (ident.Ident, error) {
return &current, nil
}
func (s *mercurySource) readIdentBasic(r *http.Request) (ident.Ident, error) {
ctx, span := lg.Span(r.Context())
defer span.End()
user, pass, ok := r.BasicAuth()
if !ok {
return nil, nil
}
id := &mercuryIdent{
identity: user,
passwd: []byte(pass),
}
space := id.Space()
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
if err != nil {
span.RecordError(err)
return id, err
}
var current mercuryIdent
current.FromConfig(c)
if len(current.passwd) == 0 {
return nil, fmt.Errorf("not registered")
}
_, err = s.idm.Passwd(id.passwd, current.passwd)
if err != nil {
return id, err
}
current.SessionInfo, err = s.idm.NewSessionInfo()
if err != nil {
return id, err
}
err = s.r.WriteConfig(ctx, current.ToConfig())
if err != nil {
return &current, err
}
return &current, nil
}
func (s *mercurySource) readIdentHTTP(r *http.Request) (ident.Ident, error) {
ctx, span := lg.Span(r.Context())
defer span.End()
if r.Method != http.MethodPost {
return nil, fmt.Errorf("method not allowed")
}
r.ParseForm()
id := &mercuryIdent{
identity: r.Form.Get("identity"),
passwd: []byte(r.Form.Get("passwd")),
}
if id.identity == "" {
return nil, nil
}
space := id.Space()
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
if err != nil {
span.RecordError(err)
return id, err
}
var current mercuryIdent
current.FromConfig(c)
if len(current.passwd) == 0 {
return nil, fmt.Errorf("not registered")
}
_, err = s.idm.Passwd(id.passwd, current.passwd)
if err != nil {
return id, err
}
current.SessionInfo, err = s.idm.NewSessionInfo()
if err != nil {
return id, err
}
err = s.r.WriteConfig(ctx, current.ToConfig())
if err != nil {
return &current, err
}
return &current, nil
}
func (s *mercurySource) RegisterIdent(ctx context.Context, identity, display string, passwd []byte) (ident.Ident, error) {
ctx, span := lg.Span(ctx)
defer span.End()

83
ident/source/session.go Normal file
View File

@@ -0,0 +1,83 @@
package source
import (
"context"
"net/http"
"time"
"github.com/oklog/ulid/v2"
"go.sour.is/pkg/ident"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/locker"
)
const CookieName = "sour.is-ident"
type sessions map[ulid.ULID]ident.Ident
type session struct {
cookieName string
sessions *locker.Locked[sessions]
}
func NewSession(cookieName string) *session {
return &session{
cookieName: cookieName,
sessions: locker.New(make(sessions)),
}
}
func (s *session) ReadIdent(r *http.Request) (ident.Ident, error) {
ctx, span := lg.Span(r.Context())
defer span.End()
cookie, err := r.Cookie(s.cookieName)
span.RecordError(err)
if err != nil {
return nil, nil
}
sessionID, err := ulid.Parse(cookie.Value)
span.RecordError(err)
var id ident.Ident = ident.Anonymous
if err == nil {
err = s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
if session, ok := sessions[sessionID]; ok {
id = session
}
return nil
})
}
span.RecordError(err)
return id, err
}
func (s *session) CreateSession(ctx context.Context, w http.ResponseWriter, id ident.Ident) error {
http.SetCookie(w, &http.Cookie{
Name: s.cookieName,
Value: id.Session().SessionID.String(),
Expires: time.Time{},
Path: "/",
Secure: false,
HttpOnly: true,
})
return s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
sessions[id.Session().SessionID] = id
return nil
})
}
func (s *session) DestroySession(ctx context.Context, w http.ResponseWriter, id ident.Ident) error {
session := id.Session()
session.Active = false
http.SetCookie(w, &http.Cookie{Name: s.cookieName, MaxAge: -1})
return s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
delete(sessions, session.SessionID)
return nil
})
}