From 33c7ac9b0689deb291b21f3f1175511a551e988e Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Thu, 29 Aug 2024 17:57:31 +0100 Subject: [PATCH] some stuff --- README.md | 6 ++- conf/conf.go | 16 +++---- config.example.yml | 47 ++++++++++++++++++ .../20240820202502_merge-auth.down.sql | 0 .../20240820202502_merge-auth.up.sql | 0 database/types/userlocale.go | 46 ++++++++++++++++++ database/types/userlocale_test.go | 12 +++++ database/types/userpronoun.go | 46 ++++++++++++++++++ database/types/userpronoun_test.go | 15 ++++++ database/types/userrole.go | 27 +++++++++++ database/types/userzone.go | 48 +++++++++++++++++++ database/types/userzone_test.go | 14 ++++++ database/types/utils_test.go | 11 +++++ go.mod | 1 + go.sum | 2 + issuer/manager.go | 12 ++--- issuer/sso.go | 5 +- sqlc.yaml | 13 +++++ 18 files changed, 303 insertions(+), 18 deletions(-) create mode 100644 config.example.yml create mode 100644 database/migrations/20240820202502_merge-auth.down.sql create mode 100644 database/migrations/20240820202502_merge-auth.up.sql create mode 100644 database/types/userlocale.go create mode 100644 database/types/userlocale_test.go create mode 100644 database/types/userpronoun.go create mode 100644 database/types/userpronoun_test.go create mode 100644 database/types/userrole.go create mode 100644 database/types/userzone.go create mode 100644 database/types/userzone_test.go create mode 100644 database/types/utils_test.go diff --git a/README.md b/README.md index 31470c8..94f72f2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # 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. diff --git a/conf/conf.go b/conf/conf.go index fd8f314..28d17a3 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -6,12 +6,12 @@ import ( ) type Conf struct { - Listen string `yaml:"listen"` - BaseUrl string `yaml:"baseUrl"` - ServiceName string `yaml:"serviceName"` - Issuer string `yaml:"issuer"` - Kid string `yaml:"kid"` - Namespace string `yaml:"namespace"` - Mail mail.Mail `yaml:"mail"` - SsoServices []issuer.SsoConfig `yaml:"ssoServices"` + Listen string `yaml:"listen"` + BaseUrl string `yaml:"baseUrl"` + ServiceName string `yaml:"serviceName"` + Issuer string `yaml:"issuer"` + Kid string `yaml:"kid"` + Namespace string `yaml:"namespace"` + Mail mail.Mail `yaml:"mail"` + SsoServices map[string]issuer.SsoConfig `yaml:"ssoServices"` } diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..f410a1b --- /dev/null +++ b/config.example.yml @@ -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 ' + 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 diff --git a/database/migrations/20240820202502_merge-auth.down.sql b/database/migrations/20240820202502_merge-auth.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/database/migrations/20240820202502_merge-auth.up.sql b/database/migrations/20240820202502_merge-auth.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/database/types/userlocale.go b/database/types/userlocale.go new file mode 100644 index 0000000..34a73b2 --- /dev/null +++ b/database/types/userlocale.go @@ -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) +} diff --git a/database/types/userlocale_test.go b/database/types/userlocale_test.go new file mode 100644 index 0000000..cc53f80 --- /dev/null +++ b/database/types/userlocale_test.go @@ -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})) +} diff --git a/database/types/userpronoun.go b/database/types/userpronoun.go new file mode 100644 index 0000000..d4068ce --- /dev/null +++ b/database/types/userpronoun.go @@ -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) +} diff --git a/database/types/userpronoun_test.go b/database/types/userpronoun_test.go new file mode 100644 index 0000000..ace183d --- /dev/null +++ b/database/types/userpronoun_test.go @@ -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})) +} diff --git a/database/types/userrole.go b/database/types/userrole.go new file mode 100644 index 0000000..85fadfd --- /dev/null +++ b/database/types/userrole.go @@ -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 +} diff --git a/database/types/userzone.go b/database/types/userzone.go new file mode 100644 index 0000000..f203ad2 --- /dev/null +++ b/database/types/userzone.go @@ -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) +} diff --git a/database/types/userzone_test.go b/database/types/userzone_test.go new file mode 100644 index 0000000..a1f2ef5 --- /dev/null +++ b/database/types/userzone_test.go @@ -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})) +} diff --git a/database/types/utils_test.go b/database/types/utils_test.go new file mode 100644 index 0000000..9f56874 --- /dev/null +++ b/database/types/utils_test.go @@ -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) +} diff --git a/go.mod b/go.mod index 2681e02..efb0d14 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/google/uuid v1.6.0 github.com/julienschmidt/httprouter v1.3.0 github.com/mattn/go-sqlite3 v1.14.22 + github.com/mrmelon54/pronouns v1.0.3 github.com/spf13/afero v1.11.0 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.26.0 diff --git a/go.sum b/go.sum index a260d02..a362a74 100644 --- a/go.sum +++ b/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/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= 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/go.mod h1:q415n6W1xcePPP5Rix6FOiADgcN66BYMyNOsFnNyoWQ= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= diff --git a/issuer/manager.go b/issuer/manager.go index b37b8b1..87b74dc 100644 --- a/issuer/manager.go +++ b/issuer/manager.go @@ -12,20 +12,20 @@ type Manager struct { 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)} - for _, i := range services { - if !isValidNamespace.MatchString(i.Namespace) { - return nil, fmt.Errorf("invalid namespace: %s", i.Namespace) + for namespace, ssoService := range services { + if !isValidNamespace.MatchString(namespace) { + return nil, fmt.Errorf("invalid namespace: %s", namespace) } - conf, err := i.FetchConfig() + conf, err := ssoService.FetchConfig() if err != nil { return nil, err } // save by namespace - l.m[i.Namespace] = conf + l.m[namespace] = conf } return l, nil } diff --git a/issuer/sso.go b/issuer/sso.go index 5d7d521..4db6feb 100644 --- a/issuer/sso.go +++ b/issuer/sso.go @@ -17,9 +17,8 @@ var httpGet = http.Get // SsoConfig is the base URL for an OAUTH/OPENID/SSO login service // The path `/.well-known/openid-configuration` should be available type SsoConfig struct { - Addr utils.JsonUrl `json:"addr"` // https://login.example.com - Namespace string `json:"namespace"` // example.com - Client SsoConfigClient `json:"client"` + Addr utils.JsonUrl `json:"addr"` // https://login.example.com + Client SsoConfigClient `json:"client"` } type SsoConfigClient struct { diff --git a/sqlc.yaml b/sqlc.yaml index 7e08599..5ace449 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -8,3 +8,16 @@ sql: package: "database" out: "database" 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"