mirror of
https://github.com/1f349/lavender.git
synced 2024-12-22 15:44:07 +00:00
some stuff
This commit is contained in:
parent
a81aa0458a
commit
33c7ac9b06
@ -1,3 +1,7 @@
|
|||||||
# Lavender
|
# Lavender
|
||||||
|
|
||||||
An authentication source for multiple login services to be used with a frontend single page application.
|
A login service with OpenID, OAuth2 and SSO support.
|
||||||
|
|
||||||
|
Login via third-party services.
|
||||||
|
|
||||||
|
Enables easy use of a single authentication source for a network of services.
|
||||||
|
16
conf/conf.go
16
conf/conf.go
@ -6,12 +6,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Conf struct {
|
type Conf struct {
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
BaseUrl string `yaml:"baseUrl"`
|
BaseUrl string `yaml:"baseUrl"`
|
||||||
ServiceName string `yaml:"serviceName"`
|
ServiceName string `yaml:"serviceName"`
|
||||||
Issuer string `yaml:"issuer"`
|
Issuer string `yaml:"issuer"`
|
||||||
Kid string `yaml:"kid"`
|
Kid string `yaml:"kid"`
|
||||||
Namespace string `yaml:"namespace"`
|
Namespace string `yaml:"namespace"`
|
||||||
Mail mail.Mail `yaml:"mail"`
|
Mail mail.Mail `yaml:"mail"`
|
||||||
SsoServices []issuer.SsoConfig `yaml:"ssoServices"`
|
SsoServices map[string]issuer.SsoConfig `yaml:"ssoServices"`
|
||||||
}
|
}
|
||||||
|
47
config.example.yml
Normal file
47
config.example.yml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# address to listen on
|
||||||
|
listen: ':9090'
|
||||||
|
|
||||||
|
# url for absolute links to the login service
|
||||||
|
baseUrl: 'http://localhost:9090'
|
||||||
|
|
||||||
|
# human-readable service name
|
||||||
|
serviceName: 'Example Login'
|
||||||
|
|
||||||
|
# name of the login issuer
|
||||||
|
issuer: 'id.example.com'
|
||||||
|
|
||||||
|
# id of the private key in the keystore
|
||||||
|
kid: 'fdd2eb6d-b469-44c8-b15b-495bcf34dae4'
|
||||||
|
|
||||||
|
# defines the domain part of login name `user@example.com`
|
||||||
|
namespace: 'example.com'
|
||||||
|
|
||||||
|
# configure automated emails
|
||||||
|
mail:
|
||||||
|
name: 'Example Login'
|
||||||
|
tls: true
|
||||||
|
server: 'smtp.example.com:465'
|
||||||
|
from: 'Example Login <noreply@id.example.com>'
|
||||||
|
username: 'noreply@id.example.com'
|
||||||
|
password: '#####'
|
||||||
|
|
||||||
|
# enable local accounts
|
||||||
|
localLogin: true
|
||||||
|
|
||||||
|
# configure SSO login services
|
||||||
|
ssoServices:
|
||||||
|
example.net:
|
||||||
|
addr: 'https://example.net'
|
||||||
|
client:
|
||||||
|
id: 'dcea4be8-dff4-49d2-a5e6-c1b202403714'
|
||||||
|
secret: '#####'
|
||||||
|
scopes:
|
||||||
|
- openid
|
||||||
|
- name
|
||||||
|
- username
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
- birthdate
|
||||||
|
- age
|
||||||
|
- zoneinfo
|
||||||
|
- locale
|
46
database/types/userlocale.go
Normal file
46
database/types/userlocale.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ sql.Scanner = &UserLocale{}
|
||||||
|
_ driver.Valuer = &UserLocale{}
|
||||||
|
_ json.Marshaler = &UserLocale{}
|
||||||
|
_ json.Unmarshaler = &UserLocale{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserLocale struct{ language.Tag }
|
||||||
|
|
||||||
|
func (l *UserLocale) Scan(src any) error {
|
||||||
|
s, ok := src.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, l)
|
||||||
|
}
|
||||||
|
lang, err := language.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l.Tag = lang
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l UserLocale) Value() (driver.Value, error) {
|
||||||
|
return l.Tag.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l UserLocale) MarshalJSON() ([]byte, error) { return json.Marshal(l.Tag.String()) }
|
||||||
|
|
||||||
|
func (l *UserLocale) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var a string
|
||||||
|
err := json.Unmarshal(bytes, &a)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return l.Scan(a)
|
||||||
|
}
|
12
database/types/userlocale_test.go
Normal file
12
database/types/userlocale_test.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserLocale_MarshalJSON(t *testing.T) {
|
||||||
|
assert.Equal(t, "\"en-US\"", encode(UserLocale{language.AmericanEnglish}))
|
||||||
|
assert.Equal(t, "\"en-GB\"", encode(UserLocale{language.BritishEnglish}))
|
||||||
|
}
|
46
database/types/userpronoun.go
Normal file
46
database/types/userpronoun.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/mrmelon54/pronouns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ sql.Scanner = &UserPronoun{}
|
||||||
|
_ driver.Valuer = &UserPronoun{}
|
||||||
|
_ json.Marshaler = &UserPronoun{}
|
||||||
|
_ json.Unmarshaler = &UserPronoun{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserPronoun struct{ pronouns.Pronoun }
|
||||||
|
|
||||||
|
func (p *UserPronoun) Scan(src any) error {
|
||||||
|
s, ok := src.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, p)
|
||||||
|
}
|
||||||
|
pro, err := pronouns.FindPronoun(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Pronoun = pro
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p UserPronoun) Value() (driver.Value, error) {
|
||||||
|
return p.Pronoun.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p UserPronoun) MarshalJSON() ([]byte, error) { return json.Marshal(p.Pronoun.String()) }
|
||||||
|
|
||||||
|
func (p *UserPronoun) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var a string
|
||||||
|
err := json.Unmarshal(bytes, &a)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.Scan(a)
|
||||||
|
}
|
15
database/types/userpronoun_test.go
Normal file
15
database/types/userpronoun_test.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mrmelon54/pronouns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserPronoun_MarshalJSON(t *testing.T) {
|
||||||
|
assert.Equal(t, "\"they/them\"", encode(UserPronoun{pronouns.TheyThem}))
|
||||||
|
assert.Equal(t, "\"he/him\"", encode(UserPronoun{pronouns.HeHim}))
|
||||||
|
assert.Equal(t, "\"she/her\"", encode(UserPronoun{pronouns.SheHer}))
|
||||||
|
assert.Equal(t, "\"it/its\"", encode(UserPronoun{pronouns.ItIts}))
|
||||||
|
assert.Equal(t, "\"one/one's\"", encode(UserPronoun{pronouns.OneOnes}))
|
||||||
|
}
|
27
database/types/userrole.go
Normal file
27
database/types/userrole.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type UserRole int64
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleMember UserRole = iota
|
||||||
|
RoleAdmin
|
||||||
|
RoleToDelete
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r UserRole) String() string {
|
||||||
|
switch r {
|
||||||
|
case RoleMember:
|
||||||
|
return "Member"
|
||||||
|
case RoleAdmin:
|
||||||
|
return "Admin"
|
||||||
|
case RoleToDelete:
|
||||||
|
return "ToDelete"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("UserRole{ %d }", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r UserRole) IsValid() bool {
|
||||||
|
return r == RoleMember || r == RoleAdmin
|
||||||
|
}
|
48
database/types/userzone.go
Normal file
48
database/types/userzone.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ sql.Scanner = &UserZone{}
|
||||||
|
_ driver.Valuer = &UserZone{}
|
||||||
|
_ json.Marshaler = &UserZone{}
|
||||||
|
_ json.Unmarshaler = &UserZone{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserZone struct{ *time.Location }
|
||||||
|
|
||||||
|
func (l *UserZone) Scan(src any) error {
|
||||||
|
s, ok := src.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, l)
|
||||||
|
}
|
||||||
|
loc, err := time.LoadLocation(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l.Location = loc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l UserZone) Value() (driver.Value, error) {
|
||||||
|
return l.Location.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l UserZone) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(l.Location.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *UserZone) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var a string
|
||||||
|
err := json.Unmarshal(bytes, &a)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return l.Scan(a)
|
||||||
|
}
|
14
database/types/userzone_test.go
Normal file
14
database/types/userzone_test.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserZone_MarshalJSON(t *testing.T) {
|
||||||
|
location, err := time.LoadLocation("Europe/London")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "\"Europe/London\"", encode(UserZone{location}))
|
||||||
|
assert.Equal(t, "\"UTC\"", encode(UserZone{time.UTC}))
|
||||||
|
}
|
11
database/types/utils_test.go
Normal file
11
database/types/utils_test.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
func encode(data any) string {
|
||||||
|
j, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(j)
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -19,6 +19,7 @@ require (
|
|||||||
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.22
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
github.com/mrmelon54/pronouns v1.0.3
|
||||||
github.com/spf13/afero v1.11.0
|
github.com/spf13/afero v1.11.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
golang.org/x/crypto v0.26.0
|
golang.org/x/crypto v0.26.0
|
||||||
|
2
go.sum
2
go.sum
@ -120,6 +120,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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/mrmelon54/pronouns v1.0.3 h1:VJqOnNxIw44q0dRJrBEvOCkKPYGvPYcNRKwPtLildXg=
|
||||||
|
github.com/mrmelon54/pronouns v1.0.3/go.mod h1:VF6iGNf72tIokVE78GasPXvxlFUwGib7QFZOfpDNn18=
|
||||||
github.com/mrmelon54/rescheduler v0.0.3 h1:TrkJL6S7PKvXuo1mvdgRgsILA/pk5L1lrXhV/q7IEzQ=
|
github.com/mrmelon54/rescheduler v0.0.3 h1:TrkJL6S7PKvXuo1mvdgRgsILA/pk5L1lrXhV/q7IEzQ=
|
||||||
github.com/mrmelon54/rescheduler v0.0.3/go.mod h1:q415n6W1xcePPP5Rix6FOiADgcN66BYMyNOsFnNyoWQ=
|
github.com/mrmelon54/rescheduler v0.0.3/go.mod h1:q415n6W1xcePPP5Rix6FOiADgcN66BYMyNOsFnNyoWQ=
|
||||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||||
|
@ -12,20 +12,20 @@ type Manager struct {
|
|||||||
m map[string]*WellKnownOIDC
|
m map[string]*WellKnownOIDC
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(services []SsoConfig) (*Manager, error) {
|
func NewManager(services map[string]SsoConfig) (*Manager, error) {
|
||||||
l := &Manager{m: make(map[string]*WellKnownOIDC)}
|
l := &Manager{m: make(map[string]*WellKnownOIDC)}
|
||||||
for _, i := range services {
|
for namespace, ssoService := range services {
|
||||||
if !isValidNamespace.MatchString(i.Namespace) {
|
if !isValidNamespace.MatchString(namespace) {
|
||||||
return nil, fmt.Errorf("invalid namespace: %s", i.Namespace)
|
return nil, fmt.Errorf("invalid namespace: %s", namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
conf, err := i.FetchConfig()
|
conf, err := ssoService.FetchConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// save by namespace
|
// save by namespace
|
||||||
l.m[i.Namespace] = conf
|
l.m[namespace] = conf
|
||||||
}
|
}
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,8 @@ var httpGet = http.Get
|
|||||||
// SsoConfig is the base URL for an OAUTH/OPENID/SSO login service
|
// SsoConfig is the base URL for an OAUTH/OPENID/SSO login service
|
||||||
// The path `/.well-known/openid-configuration` should be available
|
// The path `/.well-known/openid-configuration` should be available
|
||||||
type SsoConfig struct {
|
type SsoConfig struct {
|
||||||
Addr utils.JsonUrl `json:"addr"` // https://login.example.com
|
Addr utils.JsonUrl `json:"addr"` // https://login.example.com
|
||||||
Namespace string `json:"namespace"` // example.com
|
Client SsoConfigClient `json:"client"`
|
||||||
Client SsoConfigClient `json:"client"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SsoConfigClient struct {
|
type SsoConfigClient struct {
|
||||||
|
13
sqlc.yaml
13
sqlc.yaml
@ -8,3 +8,16 @@ sql:
|
|||||||
package: "database"
|
package: "database"
|
||||||
out: "database"
|
out: "database"
|
||||||
emit_json_tags: true
|
emit_json_tags: true
|
||||||
|
overrides:
|
||||||
|
- column: "users.password"
|
||||||
|
go_type: "github.com/1f349/tulip/password.HashString"
|
||||||
|
- column: "users.birthdate"
|
||||||
|
go_type: "github.com/hardfinhq/go-date.NullDate"
|
||||||
|
- column: "users.role"
|
||||||
|
go_type: "github.com/1f349/tulip/database/types.UserRole"
|
||||||
|
- column: "users.pronouns"
|
||||||
|
go_type: "github.com/1f349/tulip/database/types.UserPronoun"
|
||||||
|
- column: "users.zoneinfo"
|
||||||
|
go_type: "github.com/1f349/tulip/database/types.UserZone"
|
||||||
|
- column: "users.locale"
|
||||||
|
go_type: "github.com/1f349/tulip/database/types.UserLocale"
|
||||||
|
Loading…
Reference in New Issue
Block a user