1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-12-12 05:20:26 +00:00

Make account system concurrent-safe and refactor it a little

This commit is contained in:
bouncepaw 2021-01-10 01:49:48 +05:00
parent 184aa1ae32
commit b1f33c872c
19 changed files with 385 additions and 358 deletions

View File

@ -51,7 +51,7 @@ func parseCliArgs() {
case "none": case "none":
case "fixed": case "fixed":
user.AuthUsed = true user.AuthUsed = true
user.PopulateFixedUserStorage() user.ReadUsersFromFilesystem()
default: default:
log.Fatal("Error: unknown auth method:", util.AuthMethod) log.Fatal("Error: unknown auth method:", util.AuthMethod)
} }

1
go.mod
View File

@ -6,5 +6,6 @@ require (
github.com/adrg/xdg v0.2.2 github.com/adrg/xdg v0.2.2
github.com/gorilla/feeds v1.1.1 github.com/gorilla/feeds v1.1.1
github.com/hashicorp/go-memdb v1.3.0 github.com/hashicorp/go-memdb v1.3.0
github.com/kr/pretty v0.2.1 // indirect
github.com/valyala/quicktemplate v1.6.3 github.com/valyala/quicktemplate v1.6.3
) )

11
go.sum
View File

@ -1,6 +1,7 @@
github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo= github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo=
github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
@ -8,14 +9,22 @@ github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrh
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.3.0 h1:xdXq34gBOMEloa9rlGStLxmfX/dyIK8htOv36dQUwHU= github.com/hashicorp/go-memdb v1.3.0 h1:xdXq34gBOMEloa9rlGStLxmfX/dyIK8htOv36dQUwHU=
github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g= github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g=
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
@ -29,5 +38,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -121,8 +121,7 @@ func (hop *HistoryOp) WithMsg(userMsg string) *HistoryOp {
// WithUser sets a user for the commit. // WithUser sets a user for the commit.
func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp { func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp {
u = u.OrAnon() if u.Group != "anon" {
if u.Group != user.UserAnon {
hop.name = u.Name hop.name = u.Name
hop.email = u.Name + "@mycorrhiza" hop.email = u.Name + "@mycorrhiza"
} }

View File

@ -44,7 +44,7 @@ func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
newName = CanonicalName(rq.PostFormValue("new-name")) newName = CanonicalName(rq.PostFormValue("new-name"))
_, newNameIsUsed = HyphaStorage[newName] _, newNameIsUsed = HyphaStorage[newName]
recursive = rq.PostFormValue("recursive") == "true" recursive = rq.PostFormValue("recursive") == "true"
u = user.FromRequest(rq).OrAnon() u = user.FromRequest(rq)
) )
switch { switch {
case !u.CanProceed("rename-confirm"): case !u.CanProceed("rename-confirm"):
@ -151,7 +151,7 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
var ( var (
hyphaName = HyphaNameFromRq(rq, "upload-text") hyphaName = HyphaNameFromRq(rq, "upload-text")
textData = rq.PostFormValue("text") textData = rq.PostFormValue("text")
u = user.FromRequest(rq).OrAnon() u = user.FromRequest(rq)
) )
if ok := user.CanProceed(rq, "upload-text"); !ok { if ok := user.CanProceed(rq, "upload-text"); !ok {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.") HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
@ -174,7 +174,7 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL) log.Println(rq.URL)
var ( var (
hyphaName = HyphaNameFromRq(rq, "upload-binary") hyphaName = HyphaNameFromRq(rq, "upload-binary")
u = user.FromRequest(rq).OrAnon() u = user.FromRequest(rq)
) )
if !u.CanProceed("upload-binary") { if !u.CanProceed("upload-binary") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to upload attachments.") HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to upload attachments.")

View File

@ -1,7 +1,7 @@
package hyphae package hyphae
import ( import (
"github.com/hashicorp/go-memdb" "github.com/bouncepaw/mycorrhiza/storage"
) )
type Hypha struct { type Hypha struct {
@ -15,7 +15,7 @@ type Hypha struct {
// AddHypha adds a hypha named `name` with such `textPath` and `binaryPath`. Both paths can be empty. Does //not// check for hypha's existence beforehand. Count is handled. // AddHypha adds a hypha named `name` with such `textPath` and `binaryPath`. Both paths can be empty. Does //not// check for hypha's existence beforehand. Count is handled.
func AddHypha(name, textPath, binaryPath string) { func AddHypha(name, textPath, binaryPath string) {
txn := db.Txn(true) txn := storage.DB.Txn(true)
txn.Insert("hyphae", txn.Insert("hyphae",
&Hypha{ &Hypha{
Name: name, Name: name,
@ -31,54 +31,3 @@ func AddHypha(name, textPath, binaryPath string) {
// DeleteHypha clears both paths and all out-links from the named hypha and marks it as non-existent. It does not actually delete it from the memdb. Count is handled. // DeleteHypha clears both paths and all out-links from the named hypha and marks it as non-existent. It does not actually delete it from the memdb. Count is handled.
func DeleteHypha(name string) { func DeleteHypha(name string) {
} }
// Create the DB schema
var schema = &memdb.DBSchema{
Tables: map[string]*memdb.TableSchema{
"hyphae": &memdb.TableSchema{
Name: "hyphae",
Indexes: map[string]*memdb.IndexSchema{
"id": &memdb.IndexSchema{
Name: "id",
Unique: true,
Indexer: &memdb.StringFieldIndex{Field: "Name"},
},
"exists": &memdb.IndexSchema{
Name: "exists",
Unique: false,
Indexer: &memdb.BoolFieldIndex{Field: "Exists"},
},
"text-path": &memdb.IndexSchema{
Name: "text-path",
Unique: true,
Indexer: &memdb.StringFieldIndex{Field: "TextPath"},
},
"binary-path": &memdb.IndexSchema{
Name: "binary-path",
Unique: true,
Indexer: &memdb.StringFieldIndex{Field: "BinaryPath"},
},
"out-links": &memdb.IndexSchema{
Name: "out-links",
Unique: false,
Indexer: &memdb.StringSliceFieldIndex{Field: "OutLinks"},
},
"back-links": &memdb.IndexSchema{
Name: "back-links",
Unique: false,
Indexer: &memdb.StringSliceFieldIndex{Field: "BackLinks"},
},
},
},
},
}
var db *memdb.MemDB
func init() {
var err error
db, err = memdb.NewMemDB(schema)
if err != nil {
panic(err)
}
}

View File

@ -120,6 +120,9 @@ func ParagraphToHtml(hyphaName, input string) string {
startsWith = func(t string) bool { startsWith = func(t string) bool {
return bytes.HasPrefix(p.Bytes(), []byte(t)) return bytes.HasPrefix(p.Bytes(), []byte(t))
} }
noTagsActive = func() bool {
return !(tagState[spanItalic] || tagState[spanBold] || tagState[spanMono] || tagState[spanSuper] || tagState[spanSub] || tagState[spanMark] || tagState[spanLink])
}
) )
for p.Len() != 0 { for p.Len() != 0 {
@ -147,7 +150,7 @@ func ParagraphToHtml(hyphaName, input string) string {
p.Next(2) p.Next(2)
case startsWith("[["): case startsWith("[["):
ret.WriteString(getLinkNode(p, hyphaName, true)) ret.WriteString(getLinkNode(p, hyphaName, true))
case startsWith("https://"), startsWith("http://"), startsWith("gemini://"), startsWith("gopher://"), startsWith("ftp://"): case (startsWith("https://") || startsWith("http://") || startsWith("gemini://") || startsWith("gopher://") || startsWith("ftp://")) && noTagsActive():
ret.WriteString(getLinkNode(p, hyphaName, false)) ret.WriteString(getLinkNode(p, hyphaName, false))
default: default:
ret.WriteString(html.EscapeString(getTextNode(p))) ret.WriteString(html.EscapeString(getTextNode(p)))

65
storage/storage.go Normal file
View File

@ -0,0 +1,65 @@
package storage
import (
"github.com/hashicorp/go-memdb"
)
// Create the DB schema
var schema = &memdb.DBSchema{
Tables: map[string]*memdb.TableSchema{
"hyphae": &memdb.TableSchema{
Name: "hyphae",
Indexes: map[string]*memdb.IndexSchema{
"id": &memdb.IndexSchema{
Name: "id",
Unique: true,
Indexer: &memdb.StringFieldIndex{Field: "Name"},
},
"exists": &memdb.IndexSchema{
Name: "exists",
Unique: false,
Indexer: &memdb.BoolFieldIndex{Field: "Exists"},
},
"out-links": &memdb.IndexSchema{
Name: "out-links",
Unique: false,
Indexer: &memdb.StringSliceFieldIndex{Field: "OutLinks"},
},
"back-links": &memdb.IndexSchema{
Name: "back-links",
Unique: false,
Indexer: &memdb.StringSliceFieldIndex{Field: "BackLinks"},
},
},
},
},
}
var DB *memdb.MemDB
func init() {
var err error
DB, err = memdb.NewMemDB(schema)
if err != nil {
panic(err)
}
}
func ForEveryRecord(table string, λ func(obj interface{})) error {
txn := DB.Txn(false)
defer txn.Abort()
it, err := txn.Get(table, "id")
if err != nil {
return err
}
for obj := it.Next(); obj != nil; obj = it.Next() {
λ(obj)
}
return nil
}
func TxnW() *memdb.Txn { return DB.Txn(true) }
func TxnR() *memdb.Txn { return DB.Txn(false) }

View File

@ -6,7 +6,7 @@
{% if user.AuthUsed %} {% if user.AuthUsed %}
<h1>Login</h1> <h1>Login</h1>
<form method="post" action="/login-data" id="login-form" enctype="multipart/form-data"> <form method="post" action="/login-data" id="login-form" enctype="multipart/form-data">
<p>Use the data you were given by the administrator.</p> <p>Use the data you were given by an administrator.</p>
<fieldset> <fieldset>
<legend>Username</legend> <legend>Username</legend>
<input type="text" required autofocus name="username" autocomplete="on"> <input type="text" required autofocus name="username" autocomplete="on">
@ -15,7 +15,7 @@
<legend>Password</legend> <legend>Password</legend>
<input type="password" required name="password" autocomplete="on"> <input type="password" required name="password" autocomplete="on">
</fieldset> </fieldset>
<p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.</p> <p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you. You will stay logged in until you log out.</p>
<input type="submit"> <input type="submit">
<a href="/">Cancel</a> <a href="/">Cancel</a>
</form> </form>

View File

@ -33,7 +33,7 @@ func StreamLoginHTML(qw422016 *qt422016.Writer) {
qw422016.N().S(` qw422016.N().S(`
<h1>Login</h1> <h1>Login</h1>
<form method="post" action="/login-data" id="login-form" enctype="multipart/form-data"> <form method="post" action="/login-data" id="login-form" enctype="multipart/form-data">
<p>Use the data you were given by the administrator.</p> <p>Use the data you were given by an administrator.</p>
<fieldset> <fieldset>
<legend>Username</legend> <legend>Username</legend>
<input type="text" required autofocus name="username" autocomplete="on"> <input type="text" required autofocus name="username" autocomplete="on">
@ -42,7 +42,7 @@ func StreamLoginHTML(qw422016 *qt422016.Writer) {
<legend>Password</legend> <legend>Password</legend>
<input type="password" required name="password" autocomplete="on"> <input type="password" required name="password" autocomplete="on">
</fieldset> </fieldset>
<p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.</p> <p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you. You will stay logged in until you log out.</p>
<input type="submit"> <input type="submit">
<a href="/">Cancel</a> <a href="/">Cancel</a>
</form> </form>

View File

@ -21,8 +21,9 @@ var navEntries = []navEntry{
{% func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) %} {% func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) %}
{% code {% code
u := user.FromRequest(rq).OrAnon() u := user.FromRequest(rq)
%} %}
<nav class="navlinks"> <nav class="navlinks">
<ul> <ul>
{%- for _, entry := range navEntries -%} {%- for _, entry := range navEntries -%}
@ -30,7 +31,7 @@ var navEntries = []navEntry{
<li><b>{%s revisionHash[0] %}</b></li> <li><b>{%s revisionHash[0] %}</b></li>
{%- elseif navType == entry.path -%} {%- elseif navType == entry.path -%}
<li><b>{%s entry.title %}</b></li> <li><b>{%s entry.title %}</b></li>
{%- elseif entry.path != "revision" && u.Group.CanAccessRoute(entry.path) -%} {%- elseif entry.path != "revision" && u.CanProceed(entry.path) -%}
<li><a href="/{%s entry.path %}/{%s hyphaName %}">{%s entry.title %}</a></li> <li><a href="/{%s entry.path %}/{%s hyphaName %}">{%s entry.title %}</a></li>
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor -%}
@ -42,7 +43,7 @@ var navEntries = []navEntry{
{% func userMenuHTML(u *user.User) %} {% func userMenuHTML(u *user.User) %}
{% if user.AuthUsed %} {% if user.AuthUsed %}
<li class="navlinks__user"> <li class="navlinks__user">
{% if u.Group == user.UserAnon %} {% if u.Group == "anon" %}
<a href="/login">Login</a> <a href="/login">Login</a>
{% else %} {% else %}
<a href="/page/{%s util.UserTree %}/{%s u.Name %}">{%s u.Name %}</a> <a href="/page/{%s util.UserTree %}/{%s u.Name %}">{%s u.Name %}</a>

View File

@ -50,163 +50,164 @@ func streamnavHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, navTy
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/common.qtpl:24 //line templates/common.qtpl:24
u := user.FromRequest(rq).OrAnon() u := user.FromRequest(rq)
//line templates/common.qtpl:25 //line templates/common.qtpl:25
qw422016.N().S(` qw422016.N().S(`
<nav class="navlinks"> <nav class="navlinks">
<ul> <ul>
`) `)
//line templates/common.qtpl:28 //line templates/common.qtpl:29
for _, entry := range navEntries { for _, entry := range navEntries {
//line templates/common.qtpl:29 //line templates/common.qtpl:30
if navType == "revision" && entry.path == "revision" { if navType == "revision" && entry.path == "revision" {
//line templates/common.qtpl:29
qw422016.N().S(` <li><b>`)
//line templates/common.qtpl:30 //line templates/common.qtpl:30
qw422016.N().S(` <li><b>`)
//line templates/common.qtpl:31
qw422016.E().S(revisionHash[0]) qw422016.E().S(revisionHash[0])
//line templates/common.qtpl:30 //line templates/common.qtpl:31
qw422016.N().S(`</b></li> qw422016.N().S(`</b></li>
`) `)
//line templates/common.qtpl:31 //line templates/common.qtpl:32
} else if navType == entry.path { } else if navType == entry.path {
//line templates/common.qtpl:31 //line templates/common.qtpl:32
qw422016.N().S(` <li><b>`) qw422016.N().S(` <li><b>`)
//line templates/common.qtpl:32 //line templates/common.qtpl:33
qw422016.E().S(entry.title) qw422016.E().S(entry.title)
//line templates/common.qtpl:32 //line templates/common.qtpl:33
qw422016.N().S(`</b></li> qw422016.N().S(`</b></li>
`) `)
//line templates/common.qtpl:33 //line templates/common.qtpl:34
} else if entry.path != "revision" && u.Group.CanAccessRoute(entry.path) { } else if entry.path != "revision" && u.CanProceed(entry.path) {
//line templates/common.qtpl:33 //line templates/common.qtpl:34
qw422016.N().S(` <li><a href="/`) qw422016.N().S(` <li><a href="/`)
//line templates/common.qtpl:34 //line templates/common.qtpl:35
qw422016.E().S(entry.path) qw422016.E().S(entry.path)
//line templates/common.qtpl:34 //line templates/common.qtpl:35
qw422016.N().S(`/`) qw422016.N().S(`/`)
//line templates/common.qtpl:34 //line templates/common.qtpl:35
qw422016.E().S(hyphaName) qw422016.E().S(hyphaName)
//line templates/common.qtpl:34 //line templates/common.qtpl:35
qw422016.N().S(`">`) qw422016.N().S(`">`)
//line templates/common.qtpl:34 //line templates/common.qtpl:35
qw422016.E().S(entry.title) qw422016.E().S(entry.title)
//line templates/common.qtpl:34 //line templates/common.qtpl:35
qw422016.N().S(`</a></li> qw422016.N().S(`</a></li>
`) `)
//line templates/common.qtpl:35
}
//line templates/common.qtpl:36 //line templates/common.qtpl:36
} }
//line templates/common.qtpl:36 //line templates/common.qtpl:37
}
//line templates/common.qtpl:37
qw422016.N().S(` `) qw422016.N().S(` `)
//line templates/common.qtpl:37 //line templates/common.qtpl:38
qw422016.N().S(userMenuHTML(u)) qw422016.N().S(userMenuHTML(u))
//line templates/common.qtpl:37 //line templates/common.qtpl:38
qw422016.N().S(` qw422016.N().S(`
</ul> </ul>
</nav> </nav>
`) `)
//line templates/common.qtpl:40 //line templates/common.qtpl:41
} }
//line templates/common.qtpl:40 //line templates/common.qtpl:41
func writenavHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, navType string, revisionHash ...string) { func writenavHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, navType string, revisionHash ...string) {
//line templates/common.qtpl:40 //line templates/common.qtpl:41
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/common.qtpl:40 //line templates/common.qtpl:41
streamnavHTML(qw422016, rq, hyphaName, navType, revisionHash...) streamnavHTML(qw422016, rq, hyphaName, navType, revisionHash...)
//line templates/common.qtpl:40 //line templates/common.qtpl:41
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/common.qtpl:40 //line templates/common.qtpl:41
} }
//line templates/common.qtpl:40 //line templates/common.qtpl:41
func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) string { func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) string {
//line templates/common.qtpl:40 //line templates/common.qtpl:41
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/common.qtpl:40 //line templates/common.qtpl:41
writenavHTML(qb422016, rq, hyphaName, navType, revisionHash...) writenavHTML(qb422016, rq, hyphaName, navType, revisionHash...)
//line templates/common.qtpl:40 //line templates/common.qtpl:41
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/common.qtpl:40 //line templates/common.qtpl:41
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/common.qtpl:40 //line templates/common.qtpl:41
return qs422016 return qs422016
//line templates/common.qtpl:40 //line templates/common.qtpl:41
} }
//line templates/common.qtpl:42 //line templates/common.qtpl:43
func streamuserMenuHTML(qw422016 *qt422016.Writer, u *user.User) { func streamuserMenuHTML(qw422016 *qt422016.Writer, u *user.User) {
//line templates/common.qtpl:42 //line templates/common.qtpl:43
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/common.qtpl:43 //line templates/common.qtpl:44
if user.AuthUsed { if user.AuthUsed {
//line templates/common.qtpl:43 //line templates/common.qtpl:44
qw422016.N().S(` qw422016.N().S(`
<li class="navlinks__user"> <li class="navlinks__user">
`) `)
//line templates/common.qtpl:45 //line templates/common.qtpl:46
if u.Group == user.UserAnon { if u.Group == "anon" {
//line templates/common.qtpl:45 //line templates/common.qtpl:46
qw422016.N().S(` qw422016.N().S(`
<a href="/login">Login</a> <a href="/login">Login</a>
`) `)
//line templates/common.qtpl:47 //line templates/common.qtpl:48
} else { } else {
//line templates/common.qtpl:47 //line templates/common.qtpl:48
qw422016.N().S(` qw422016.N().S(`
<a href="/page/`) <a href="/page/`)
//line templates/common.qtpl:48 //line templates/common.qtpl:49
qw422016.E().S(util.UserTree) qw422016.E().S(util.UserTree)
//line templates/common.qtpl:48 //line templates/common.qtpl:49
qw422016.N().S(`/`) qw422016.N().S(`/`)
//line templates/common.qtpl:48 //line templates/common.qtpl:49
qw422016.E().S(u.Name) qw422016.E().S(u.Name)
//line templates/common.qtpl:48 //line templates/common.qtpl:49
qw422016.N().S(`">`) qw422016.N().S(`">`)
//line templates/common.qtpl:48 //line templates/common.qtpl:49
qw422016.E().S(u.Name) qw422016.E().S(u.Name)
//line templates/common.qtpl:48 //line templates/common.qtpl:49
qw422016.N().S(`</a> qw422016.N().S(`</a>
`) `)
//line templates/common.qtpl:49 //line templates/common.qtpl:50
} }
//line templates/common.qtpl:49 //line templates/common.qtpl:50
qw422016.N().S(` qw422016.N().S(`
</li> </li>
`) `)
//line templates/common.qtpl:51 //line templates/common.qtpl:52
} }
//line templates/common.qtpl:51 //line templates/common.qtpl:52
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/common.qtpl:52 //line templates/common.qtpl:53
} }
//line templates/common.qtpl:52 //line templates/common.qtpl:53
func writeuserMenuHTML(qq422016 qtio422016.Writer, u *user.User) { func writeuserMenuHTML(qq422016 qtio422016.Writer, u *user.User) {
//line templates/common.qtpl:52 //line templates/common.qtpl:53
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/common.qtpl:52 //line templates/common.qtpl:53
streamuserMenuHTML(qw422016, u) streamuserMenuHTML(qw422016, u)
//line templates/common.qtpl:52 //line templates/common.qtpl:53
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/common.qtpl:52 //line templates/common.qtpl:53
} }
//line templates/common.qtpl:52 //line templates/common.qtpl:53
func userMenuHTML(u *user.User) string { func userMenuHTML(u *user.User) string {
//line templates/common.qtpl:52 //line templates/common.qtpl:53
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/common.qtpl:52 //line templates/common.qtpl:53
writeuserMenuHTML(qb422016, u) writeuserMenuHTML(qb422016, u)
//line templates/common.qtpl:52 //line templates/common.qtpl:53
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/common.qtpl:52 //line templates/common.qtpl:53
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/common.qtpl:52 //line templates/common.qtpl:53
return qs422016 return qs422016
//line templates/common.qtpl:52 //line templates/common.qtpl:53
} }

View File

@ -48,7 +48,7 @@ If `contents` == "", a helpful message is shown instead.
{% endif %} {% endif %}
</section> </section>
<hr class="page-separator"/> <hr class="page-separator"/>
{% if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon %} {% if u := user.FromRequest(rq); !user.AuthUsed || u.Group != "anon" %}
<form action="/upload-binary/{%s hyphaName %}" <form action="/upload-binary/{%s hyphaName %}"
method="post" enctype="multipart/form-data"> method="post" enctype="multipart/form-data">
<label for="upload-binary__input">Upload a new attachment</label> <label for="upload-binary__input">Upload a new attachment</label>

View File

@ -228,7 +228,7 @@ func StreamPageHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, navi
<hr class="page-separator"/> <hr class="page-separator"/>
`) `)
//line templates/http_readers.qtpl:51 //line templates/http_readers.qtpl:51
if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon { if u := user.FromRequest(rq); !user.AuthUsed || u.Group != "anon" {
//line templates/http_readers.qtpl:51 //line templates/http_readers.qtpl:51
qw422016.N().S(` qw422016.N().S(`
<form action="/upload-binary/`) <form action="/upload-binary/`)

View File

@ -10,53 +10,55 @@ import (
"github.com/bouncepaw/mycorrhiza/util" "github.com/bouncepaw/mycorrhiza/util"
) )
func PopulateFixedUserStorage() { // ReadUsersFromFilesystem reads all user information from filesystem and stores it internally. Call it during initialization.
func ReadUsersFromFilesystem() {
rememberUsers(usersFromFixedCredentials())
readTokensToUsers()
}
func usersFromFixedCredentials() []*User {
var users []*User
contents, err := ioutil.ReadFile(util.FixedCredentialsPath) contents, err := ioutil.ReadFile(util.FixedCredentialsPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
err = json.Unmarshal(contents, &UserStorage.Users) err = json.Unmarshal(contents, &users)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for _, user := range UserStorage.Users { log.Println("Found", len(users), "fixed users")
user.Group = groupFromString(user.GroupString) return users
} }
log.Println("Found", len(UserStorage.Users), "fixed users")
contents, err = ioutil.ReadFile(tokenStoragePath()) func rememberUsers(uu []*User) {
// uu is used to not shadow the `users` in `users.go`.
for _, user := range uu {
users.Store(user.Name, user)
}
}
func readTokensToUsers() {
contents, err := ioutil.ReadFile(tokenStoragePath())
if os.IsNotExist(err) { if os.IsNotExist(err) {
return return
} }
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var tmp map[string]string var tmp map[string]string
err = json.Unmarshal(contents, &tmp) err = json.Unmarshal(contents, &tmp)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for token, username := range tmp { for token, username := range tmp {
user := UserStorage.userByName(username) commenceSession(username, token)
UserStorage.Tokens[token] = user
} }
log.Println("Found", len(tmp), "active sessions") log.Println("Found", len(tmp), "active sessions")
} }
func dumpTokens() { // Return path to tokens.json. Creates folders if needed.
tmp := make(map[string]string)
for token, user := range UserStorage.Tokens {
tmp[token] = user.Name
}
blob, err := json.Marshal(tmp)
if err != nil {
log.Println(err)
} else {
ioutil.WriteFile(tokenStoragePath(), blob, 0644)
}
}
// Return path to tokens.json.
func tokenStoragePath() string { func tokenStoragePath() string {
dir, err := xdg.DataFile("mycorrhiza/tokens.json") dir, err := xdg.DataFile("mycorrhiza/tokens.json")
if err != nil { if err != nil {
@ -67,3 +69,21 @@ func tokenStoragePath() string {
} }
return dir return dir
} }
func dumpTokens() {
tmp := make(map[string]string)
tokens.Range(func(k, v interface{}) bool {
token := k.(string)
username := v.(string)
tmp[token] = username
return true
})
blob, err := json.Marshal(tmp)
if err != nil {
log.Println(err)
} else {
ioutil.WriteFile(tokenStoragePath(), blob, 0644)
}
}

View File

@ -1,70 +0,0 @@
package user
import (
"log"
"net/http"
)
func groupFromString(s string) UserGroup {
switch s {
case "admin":
return UserAdmin
case "moderator":
return UserModerator
case "trusted":
return UserTrusted
case "editor":
return UserEditor
default:
log.Fatal("Unknown user group", s)
return UserAnon
}
}
// UserGroup represents a group that a user is part of.
type UserGroup int
const (
// UserAnon is the default user group which all unauthorized visitors have.
UserAnon UserGroup = iota
// UserEditor is a user who can edit and upload stuff.
UserEditor
// UserTrusted is a trusted editor who can also rename stuff.
UserTrusted
// UserModerator is a moderator who can also delete stuff.
UserModerator
// UserAdmin can do everything.
UserAdmin
)
var minimalRights = map[string]UserGroup{
"edit": UserEditor,
"upload-binary": UserEditor,
"upload-text": UserEditor,
"rename-ask": UserTrusted,
"rename-confirm": UserTrusted,
"delete-ask": UserModerator,
"delete-confirm": UserModerator,
"reindex": UserAdmin,
}
func (ug UserGroup) CanAccessRoute(route string) bool {
if !AuthUsed {
return true
}
if minimalRight, ok := minimalRights[route]; ok {
if ug >= minimalRight {
return true
}
return false
}
return true
}
func CanProceed(rq *http.Request, route string) bool {
return FromRequest(rq).OrAnon().CanProceed(route)
}
func (u *User) CanProceed(route string) bool {
return u.Group.CanAccessRoute(route)
}

75
user/net.go Normal file
View File

@ -0,0 +1,75 @@
package user
import (
"log"
"net/http"
"time"
"github.com/bouncepaw/mycorrhiza/util"
)
// CanProceed returns `true` if the user in `rq` has enough rights to access `route`.
func CanProceed(rq *http.Request, route string) bool {
return FromRequest(rq).CanProceed(route)
}
// FromRequest returns user from `rq`. If there is no user, an anon user is returned instead.
func FromRequest(rq *http.Request) *User {
cookie, err := rq.Cookie("mycorrhiza_token")
if err != nil {
return emptyUser()
}
return userByToken(cookie.Value)
}
// LogoutFromRequest logs the user in `rq` out and rewrites the cookie in `w`.
func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) {
cookieFromUser, err := rq.Cookie("mycorrhiza_token")
if err == nil {
http.SetCookie(w, cookie("token", "", time.Unix(0, 0)))
terminateSession(cookieFromUser.Value)
}
}
// LoginDataHTTP logs such user in and returns string representation of an error if there is any.
func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string {
w.Header().Set("Content-Type", "text/html;charset=utf-8")
if !HasUsername(username) {
w.WriteHeader(http.StatusBadRequest)
log.Println("Unknown username", username, "was entered")
return "unknown username"
}
if !CredentialsOK(username, password) {
w.WriteHeader(http.StatusBadRequest)
log.Println("A wrong password was entered for username", username)
return "wrong password"
}
token, err := AddSession(username)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return err.Error()
}
http.SetCookie(w, cookie("token", token, time.Now().Add(365*24*time.Hour)))
return ""
}
// AddSession saves a session for `username` and returns a token to use.
func AddSession(username string) (string, error) {
token, err := util.RandomString(16)
if err == nil {
commenceSession(username, token)
log.Println("New token for", username, "is", token)
}
return token, err
}
// A handy cookie constructor
func cookie(name_suffix, val string, t time.Time) *http.Cookie {
return &http.Cookie{
Name: "mycorrhiza_" + name_suffix,
Value: val,
Expires: t,
Path: "/",
}
}

View File

@ -1,138 +1,66 @@
package user package user
import ( import (
"log" "sync"
"net/http"
"time"
"github.com/bouncepaw/mycorrhiza/util"
) )
func (u *User) OrAnon() *User {
if u == nil {
return &User{}
}
return u
}
func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) {
cookieFromUser, err := rq.Cookie("mycorrhiza_token")
if err == nil {
http.SetCookie(w, cookie("token", "", time.Unix(0, 0)))
terminateSession(cookieFromUser.Value)
}
}
func (us *FixedUserStorage) userByToken(token string) *User {
if user, ok := us.Tokens[token]; ok {
return user
}
return nil
}
func (us *FixedUserStorage) userByName(username string) *User {
for _, user := range us.Users {
if user.Name == username {
return user
}
}
return nil
}
func FromRequest(rq *http.Request) *User {
cookie, err := rq.Cookie("mycorrhiza_token")
if err != nil {
return nil
}
return UserStorage.userByToken(cookie.Value).OrAnon()
}
func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string {
w.Header().Set("Content-Type", "text/html;charset=utf-8")
if !HasUsername(username) {
w.WriteHeader(http.StatusBadRequest)
log.Println("Unknown username", username, "was entered")
return "unknown username"
}
if !CredentialsOK(username, password) {
w.WriteHeader(http.StatusBadRequest)
log.Println("A wrong password was entered for username", username)
return "wrong password"
}
token, err := AddSession(username)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return err.Error()
}
http.SetCookie(w, cookie("token", token, time.Now().Add(14*24*time.Hour)))
return ""
}
// AddSession saves a session for `username` and returns a token to use.
func AddSession(username string) (string, error) {
token, err := util.RandomString(16)
if err == nil {
for _, user := range UserStorage.Users {
if user.Name == username {
UserStorage.Tokens[token] = user
go dumpTokens()
}
}
log.Println("New token for", username, "is", token)
}
return token, err
}
func terminateSession(token string) {
delete(UserStorage.Tokens, token)
go dumpTokens()
}
func HasUsername(username string) bool {
for _, user := range UserStorage.Users {
if user.Name == username {
return true
}
}
return false
}
func CredentialsOK(username, password string) bool {
for _, user := range UserStorage.Users {
if user.Name == username && user.Password == password {
return true
}
}
return false
}
type FixedUserStorage struct {
Users []*User
Tokens map[string]*User
}
var UserStorage = FixedUserStorage{Tokens: make(map[string]*User)}
// AuthUsed shows if a method of authentication is used. You should set it by yourself.
var AuthUsed bool
// User is a user. // User is a user.
type User struct { type User struct {
// Name is a username. It must follow hypha naming rules. // Name is a username. It must follow hypha naming rules.
Name string `json:"name"` Name string `json:"name"`
// Group the user is part of. Group string `json:"group"`
Group UserGroup `json:"-"`
GroupString string `json:"group"`
Password string `json:"password"` Password string `json:"password"`
sync.RWMutex
} }
// A handy cookie constructor // Route — Right (more is more right)
func cookie(name_suffix, val string, t time.Time) *http.Cookie { var minimalRights = map[string]int{
return &http.Cookie{ "edit": 1,
Name: "mycorrhiza_" + name_suffix, "upload-binary": 1,
Value: val, "upload-text": 1,
Expires: t, "rename-ask": 2,
Path: "/", "rename-confirm": 2,
"delete-ask": 3,
"delete-confirm": 3,
"reindex": 4,
}
// Group — Right
var groupRight = map[string]int{
"anon": 0,
"editor": 1,
"trusted": 2,
"moderator": 3,
"admin": 4,
}
func emptyUser() *User {
return &User{
Name: "anon",
Group: "anon",
Password: "",
} }
} }
func (user *User) CanProceed(route string) bool {
if !AuthUsed {
return true
}
user.RLock()
defer user.RUnlock()
right, _ := groupRight[user.Group]
minimalRight, _ := minimalRights[route]
if right >= minimalRight {
return true
}
return false
}
func (user *User) isCorrectPassword(password string) bool {
user.RLock()
defer user.RUnlock()
return password == user.Password
}

44
user/users.go Normal file
View File

@ -0,0 +1,44 @@
package user
import (
"sync"
)
var AuthUsed bool
var users sync.Map
var tokens sync.Map
func HasUsername(username string) bool {
_, has := users.Load(username)
return has
}
func CredentialsOK(username, password string) bool {
return userByName(username).isCorrectPassword(password)
}
func userByToken(token string) *User {
if usernameUntyped, ok := tokens.Load(token); ok {
username := usernameUntyped.(string)
return userByName(username)
}
return emptyUser()
}
func userByName(username string) *User {
if userUntyped, ok := users.Load(username); ok {
user := userUntyped.(*User)
return user
}
return emptyUser()
}
func commenceSession(username, token string) {
tokens.Store(token, username)
go dumpTokens()
}
func terminateSession(token string) {
tokens.Delete(token)
go dumpTokens()
}