Add public to oauth client store and separate fetching user info

This commit is contained in:
Melon 2024-02-08 01:16:14 +00:00
parent 9228f6649e
commit b99fb9df6f
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
12 changed files with 189 additions and 95 deletions

View File

@ -16,7 +16,7 @@ type User struct {
type ClientInfoDbOutput struct { type ClientInfoDbOutput struct {
Sub, Name, Secret, Domain, Owner string Sub, Name, Secret, Domain, Owner string
SSO, Active bool Public, SSO, Active bool
} }
var _ oauth2.ClientInfo = &ClientInfoDbOutput{} var _ oauth2.ClientInfo = &ClientInfoDbOutput{}
@ -24,7 +24,7 @@ var _ oauth2.ClientInfo = &ClientInfoDbOutput{}
func (c *ClientInfoDbOutput) GetID() string { return c.Sub } func (c *ClientInfoDbOutput) GetID() string { return c.Sub }
func (c *ClientInfoDbOutput) GetSecret() string { return c.Secret } func (c *ClientInfoDbOutput) GetSecret() string { return c.Secret }
func (c *ClientInfoDbOutput) GetDomain() string { return c.Domain } func (c *ClientInfoDbOutput) GetDomain() string { return c.Domain }
func (c *ClientInfoDbOutput) IsPublic() bool { return false } func (c *ClientInfoDbOutput) IsPublic() bool { return c.Public }
func (c *ClientInfoDbOutput) GetUserID() string { return c.Owner } func (c *ClientInfoDbOutput) GetUserID() string { return c.Owner }
// GetName is an extra field for the oauth handler to display the application // GetName is an extra field for the oauth handler to display the application

View File

@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS client_store
secret TEXT UNIQUE NOT NULL, secret TEXT UNIQUE NOT NULL,
domain TEXT NOT NULL, domain TEXT NOT NULL,
owner TEXT NOT NULL, owner TEXT NOT NULL,
public INTEGER,
sso INTEGER, sso INTEGER,
active INTEGER DEFAULT 1, active INTEGER DEFAULT 1,
FOREIGN KEY (owner) REFERENCES users (subject) FOREIGN KEY (owner) REFERENCES users (subject)

View File

@ -65,8 +65,8 @@ func (t *Tx) GetUserEmail(sub string) (string, error) {
func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) { func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) {
var u ClientInfoDbOutput var u ClientInfoDbOutput
row := t.tx.QueryRow(`SELECT secret, name, domain, sso, active FROM client_store WHERE subject = ? LIMIT 1`, sub) row := t.tx.QueryRow(`SELECT secret, name, domain, public, sso, active FROM client_store WHERE subject = ? LIMIT 1`, sub)
err := row.Scan(&u.Secret, &u.Name, &u.Domain, &u.SSO, &u.Active) err := row.Scan(&u.Secret, &u.Name, &u.Domain, &u.Public, &u.SSO, &u.Active)
u.Owner = sub u.Owner = sub
if !u.Active { if !u.Active {
return nil, fmt.Errorf("client is not active") return nil, fmt.Errorf("client is not active")
@ -74,16 +74,16 @@ func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) {
return &u, err return &u, err
} }
func (t *Tx) GetAppList(offset int) ([]ClientInfoDbOutput, error) { func (t *Tx) GetAppList(owner string, admin bool, offset int) ([]ClientInfoDbOutput, error) {
var u []ClientInfoDbOutput var u []ClientInfoDbOutput
row, err := t.tx.Query(`SELECT subject, name, domain, owner, sso, active FROM client_store LIMIT 25 OFFSET ?`, offset) row, err := t.tx.Query(`SELECT subject, name, domain, owner, public, sso, active FROM client_store WHERE owner = ? OR ? = 1 LIMIT 25 OFFSET ?`, owner, admin, offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer row.Close() defer row.Close()
for row.Next() { for row.Next() {
var a ClientInfoDbOutput var a ClientInfoDbOutput
err := row.Scan(&a.Sub, &a.Name, &a.Domain, &a.Owner, &a.SSO, &a.Active) err := row.Scan(&a.Sub, &a.Name, &a.Domain, &a.Owner, &a.Public, &a.SSO, &a.Active)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -92,18 +92,18 @@ func (t *Tx) GetAppList(offset int) ([]ClientInfoDbOutput, error) {
return u, row.Err() return u, row.Err()
} }
func (t *Tx) InsertClientApp(name, domain string, sso, active bool, owner string) error { func (t *Tx) InsertClientApp(name, domain string, public, sso, active bool, owner string) error {
u := uuid.New() u := uuid.New()
secret, err := password.GenerateApiSecret(70) secret, err := password.GenerateApiSecret(70)
if err != nil { if err != nil {
return err return err
} }
_, err = t.tx.Exec(`INSERT INTO client_store (subject, name, secret, domain, owner, sso, active) VALUES (?, ?, ?, ?, ?, ?, ?)`, u.String(), name, secret, domain, owner, sso, active) _, err = t.tx.Exec(`INSERT INTO client_store (subject, name, secret, domain, owner, public, sso, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, u.String(), name, secret, domain, owner, public, sso, active)
return err return err
} }
func (t *Tx) UpdateClientApp(subject uuid.UUID, owner string, name, domain string, sso, active bool) error { func (t *Tx) UpdateClientApp(subject uuid.UUID, owner string, name, domain string, public, sso, active bool) error {
_, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, sso, active, subject.String(), owner) _, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, public = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, public, sso, active, subject.String(), owner)
return err return err
} }

18
go.mod
View File

@ -13,7 +13,7 @@ require (
github.com/google/subcommands v1.2.0 github.com/google/subcommands v1.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
github.com/mattn/go-sqlite3 v1.14.18 github.com/mattn/go-sqlite3 v1.14.22
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
golang.org/x/oauth2 v0.16.0 golang.org/x/oauth2 v0.16.0
) )
@ -21,20 +21,20 @@ require (
require ( require (
github.com/MrMelon54/rescheduler v0.0.2 // indirect github.com/MrMelon54/rescheduler v0.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect github.com/tidwall/btree v1.7.0 // indirect
github.com/tidwall/buntdb v1.1.2 // indirect github.com/tidwall/buntdb v1.3.0 // indirect
github.com/tidwall/gjson v1.12.1 // indirect github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.20.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.32.0 // indirect google.golang.org/protobuf v1.32.0 // indirect

32
go.sum
View File

@ -30,8 +30,9 @@ github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQ
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ= github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg= github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -83,8 +84,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@ -112,25 +113,36 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI= github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI=
github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA=
github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
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=
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=

View File

@ -15,11 +15,13 @@
<button type="submit">Manage Applications</button> <button type="submit">Manage Applications</button>
</form> </form>
</div> </div>
{{if .IsAdmin}}
<div> <div>
<form method="GET" action="/manage/users"> <form method="GET" action="/manage/users">
<button type="submit">Manage Users</button> <button type="submit">Manage Users</button>
</form> </form>
</div> </div>
{{end}}
<div> <div>
<form method="POST" action="/logout"> <form method="POST" action="/logout">
<input type="hidden" name="nonce" value="{{.Nonce}}"> <input type="hidden" name="nonce" value="{{.Nonce}}">

View File

@ -58,15 +58,16 @@
<label for="field_domain">Domain:</label> <label for="field_domain">Domain:</label>
<input type="text" name="domain" id="field_domain" value="{{.Edit.Domain}}" required/> <input type="text" name="domain" id="field_domain" value="{{.Edit.Domain}}" required/>
</div> </div>
<div>
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public" {{if .Edit.Public}}checked{{end}}/></label>
</div>
{{if .IsAdmin}} {{if .IsAdmin}}
<div> <div>
<label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso" <label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso" {{if .Edit.SSO}}checked{{end}}/></label>
{{if .Edit.SSO}}checked{{end}}/></label>
</div> </div>
{{end}} {{end}}
<div> <div>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" <label for="field_active">Active: <input type="checkbox" name="active" id="field_active" {{if .Edit.Active}}checked{{end}}/></label>
{{if .Edit.Active}}checked{{end}}/></label>
</div> </div>
<button type="submit">Edit</button> <button type="submit">Edit</button>
</form> </form>
@ -131,14 +132,16 @@
<label for="field_domain">Domain:</label> <label for="field_domain">Domain:</label>
<input type="text" name="domain" id="field_domain" required/> <input type="text" name="domain" id="field_domain" required/>
</div> </div>
<div>
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public"/></label>
</div>
{{if .IsAdmin}} {{if .IsAdmin}}
<div> <div>
<label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso"/></label> <label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso"/></label>
</div> </div>
{{end}} {{end}}
<div> <div>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" <label for="field_active">Active: <input type="checkbox" name="active" id="field_active" checked/></label>
checked/></label>
</div> </div>
<button type="submit">Create</button> <button type="submit">Create</button>
</form> </form>

View File

@ -1,10 +1,6 @@
package server package server
import ( import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"fmt" "fmt"
"github.com/1f349/lavender/database" "github.com/1f349/lavender/database"
"github.com/go-session/session" "github.com/go-session/session"
@ -24,7 +20,7 @@ type UserAuth struct {
type SessionData struct { type SessionData struct {
ID string ID string
DisplayName string DisplayName string
UserInfo map[string]any UserInfo UserInfoFields
} }
func (u UserAuth) IsGuest() bool { func (u UserAuth) IsGuest() bool {
@ -45,7 +41,7 @@ func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Han
}) { }) {
return return
} }
if HasRole(roles, "lavender:admin") { if !HasRole(roles, "lavender:admin") {
http.Error(rw, "403 Forbidden", http.StatusForbidden) http.Error(rw, "403 Forbidden", http.StatusForbidden)
return return
} }
@ -71,14 +67,8 @@ func (h *HttpServer) OptionalAuthentication(next UserHandler) httprouter.Handle
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
} }
if auth.IsGuest() { if auth.IsGuest() && !h.readLoginDataCookie(rw, req, &auth) {
if loginCookie, err := req.Cookie("login-data"); err == nil { return
if decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value); err == nil {
if decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("login-data")); err == nil {
auth.Data.ID = string(decryptedData)
}
}
}
} }
next(rw, req, params, auth) next(rw, req, params, auth)
} }

View File

@ -1,12 +1,17 @@
package server package server
import ( import (
"bytes"
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha256" "crypto/sha256"
"database/sql"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"github.com/1f349/lavender/database"
"github.com/1f349/lavender/issuer"
"github.com/1f349/lavender/pages" "github.com/1f349/lavender/pages"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
@ -106,61 +111,52 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
return return
} }
res, err := flowState.sso.OAuth2Config.Client(context.Background(), token).Get(flowState.sso.UserInfoEndpoint) sessionData, done := h.fetchUserInfo(rw, err, flowState.sso, token)
if err != nil || res.StatusCode != 200 { if !done {
rw.WriteHeader(http.StatusInternalServerError)
if err != nil {
_, _ = rw.Write([]byte(err.Error()))
} else {
_, _ = rw.Write([]byte(res.Status))
}
return return
} }
defer res.Body.Close()
var userInfoJson map[string]any if h.DbTx(rw, func(tx *database.Tx) error {
if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil { _, err := tx.GetUser(sessionData.ID)
http.Error(rw, err.Error(), http.StatusInternalServerError) if errors.Is(err, sql.ErrNoRows) {
return uEmail := sessionData.UserInfo.GetStringOrDefault("email", "unknown@localhost")
uEmailVerified, _ := sessionData.UserInfo.GetBoolean("email_verified")
return tx.InsertUser(sessionData.ID, uEmail, uEmailVerified, "", true)
} }
subject, ok := userInfoJson["sub"].(string) return err
if !ok { }) {
http.Error(rw, "Invalid subject", http.StatusInternalServerError)
return return
} }
subject += "@" + flowState.sso.Config.Namespace
displayName, ok := userInfoJson["name"].(string)
if !ok {
displayName = "Unknown Name"
}
// only continues if the above tx succeeds // only continues if the above tx succeeds
auth.Data = SessionData{ auth.Data = sessionData
ID: subject,
DisplayName: displayName,
UserInfo: userInfoJson,
}
if auth.SaveSessionData() != nil { if auth.SaveSessionData() != nil {
http.Error(rw, "Failed to save session", http.StatusInternalServerError) http.Error(rw, "Failed to save session", http.StatusInternalServerError)
return return
} }
if h.setLoginDataCookie(rw, auth.Data.ID) { if h.setLoginDataCookie(rw, auth.Data.ID, token) {
http.Error(rw, "Internal Server Error", http.StatusInternalServerError) http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
return return
} }
h.SafeRedirect(rw, req) h.SafeRedirect(rw, req)
} }
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string) bool { func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string, token *oauth2.Token) bool {
encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), []byte(userId), []byte("login-data")) buf := new(bytes.Buffer)
buf.WriteString(userId)
buf.WriteByte(0)
err := json.NewEncoder(buf).Encode(token)
if err != nil {
return true
}
encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), buf.Bytes(), []byte("lavender-login-data"))
if err != nil { if err != nil {
return true return true
} }
encryptedString := base64.RawStdEncoding.EncodeToString(encryptedData) encryptedString := base64.RawStdEncoding.EncodeToString(encryptedData)
http.SetCookie(rw, &http.Cookie{ http.SetCookie(rw, &http.Cookie{
Name: "login-data", Name: "lavender-login-data",
Value: encryptedString, Value: encryptedString,
Path: "/", Path: "/",
Expires: time.Now().AddDate(0, 3, 0), Expires: time.Now().AddDate(0, 3, 0),
@ -169,3 +165,71 @@ func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string) b
}) })
return false return false
} }
func (h *HttpServer) readLoginDataCookie(rw http.ResponseWriter, req *http.Request, u *UserAuth) bool {
loginCookie, err := req.Cookie("lavender-login-data")
if err != nil {
return false
}
decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value)
if err != nil {
return false
}
decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("lavender-login-data"))
if err != nil {
return false
}
buf := bytes.NewBuffer(decryptedData)
userId, err := buf.ReadString(0)
if err != nil {
return false
}
userId = strings.TrimSuffix(userId, "\x00")
var token *oauth2.Token
err = json.NewDecoder(buf).Decode(&token)
if err != nil {
return false
}
sso := h.manager.FindServiceFromLogin(userId)
if sso == nil {
return false
}
sessionData, done := h.fetchUserInfo(rw, err, sso, token)
if !done {
return false
}
u.Data = sessionData
return true
}
func (h *HttpServer) fetchUserInfo(rw http.ResponseWriter, err error, sso *issuer.WellKnownOIDC, token *oauth2.Token) (SessionData, bool) {
res, err := sso.OAuth2Config.Client(context.Background(), token).Get(sso.UserInfoEndpoint)
if err != nil || res.StatusCode != http.StatusOK {
return SessionData{}, false
}
defer res.Body.Close()
var userInfoJson UserInfoFields
if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return SessionData{}, false
}
subject, ok := userInfoJson.GetString("sub")
if !ok {
http.Error(rw, "Invalid subject", http.StatusInternalServerError)
return SessionData{}, false
}
subject += "@" + sso.Config.Namespace
displayName := userInfoJson.GetStringOrDefault("name", "Unknown Name")
return SessionData{
ID: subject,
DisplayName: displayName,
UserInfo: userInfoJson,
}, true
}

View File

@ -30,7 +30,7 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
if err != nil { if err != nil {
return return
} }
appList, err = tx.GetAppList(offset) appList, err = tx.GetAppList(auth.Data.ID, HasRole(roles, "lavender:admin"), offset)
return return
}) { }) {
return return
@ -72,6 +72,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
action := req.Form.Get("action") action := req.Form.Get("action")
name := req.Form.Get("name") name := req.Form.Get("name")
domain := req.Form.Get("domain") domain := req.Form.Get("domain")
public := req.Form.Has("public")
sso := req.Form.Has("sso") sso := req.Form.Has("sso")
active := req.Form.Has("active") active := req.Form.Has("active")
@ -92,7 +93,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
switch action { switch action {
case "create": case "create":
if h.DbTx(rw, func(tx *database.Tx) error { if h.DbTx(rw, func(tx *database.Tx) error {
return tx.InsertClientApp(name, domain, sso, active, auth.Data.ID) return tx.InsertClientApp(name, domain, public, sso, active, auth.Data.ID)
}) { }) {
return return
} }
@ -102,7 +103,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
if err != nil { if err != nil {
return err return err
} }
return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, sso, active) return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, public, sso, active)
}) { }) {
return return
} }

View File

@ -137,7 +137,7 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
} }
http.SetCookie(rw, &http.Cookie{ http.SetCookie(rw, &http.Cookie{
Name: "login-data", Name: "lavender-login-data",
Path: "/", Path: "/",
MaxAge: -1, MaxAge: -1,
Secure: true, Secure: true,
@ -156,8 +156,8 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
}) })
// management pages // management pages
r.GET("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsGet)) r.GET("/manage/apps", hs.RequireAuthentication(hs.ManageAppsGet))
r.POST("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsPost)) r.POST("/manage/apps", hs.RequireAuthentication(hs.ManageAppsPost))
r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet)) r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet))
r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost)) r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost))

21
server/userinfo.go Normal file
View File

@ -0,0 +1,21 @@
package server
type UserInfoFields map[string]any
func (u UserInfoFields) GetString(key string) (string, bool) {
s, ok := u[key].(string)
return s, ok
}
func (u UserInfoFields) GetStringOrDefault(key, other string) string {
s, ok := u.GetString(key)
if !ok {
s = other
}
return s
}
func (u UserInfoFields) GetBoolean(key string) (bool, bool) {
b, ok := u[key].(bool)
return b, ok
}