diff --git a/.golangci.yml b/.golangci.yml index be95b79..7ac151f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,6 +3,8 @@ linters-settings: line-length: 200 gocyclo: min-complexity: 20 + gocognit: + min-complexity: 30 linters: enable: @@ -57,5 +59,8 @@ issues: linters: - gochecknoglobals - dupl + - path: security.go + linters: + - gocognit include: - EXC0002 \ No newline at end of file diff --git a/README.md b/README.md index de78ae3..7e80757 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Here is the list of all main features so far: - [X] Simple ticket check in NSP/NSZ (based on titledb file) - [X] Collect basic statistics - [X] An API to query information about your shop +- [X] Handle Basic Auth from Tinfoil through Forward Auth Endpoint ## Filtering @@ -118,7 +119,7 @@ Use a reverse proxy (like [traefik](https://github.com/traefik/traefik), [caddy] ### Example for caddy -To work with `caddy`, you need to put in your `Caddyfile` something similar to this: +To work with [`caddy`](https://caddyserver.com/), you need to put in your `Caddyfile` something similar to this: ```Caddyfile tinshop.example.com:80 { @@ -139,10 +140,12 @@ If you want to have HTTPS, ensure `caddy` handle it (it will with Let's Encrypt) ## How can I add a `basic auth` to protect my shop? -TinShop **does not** implement basic auth by itself. -You should configure it inside your reverse proxy. +TinShop **does** handle basic auth but not by itself. +You should look for `forwardAuth` in the `config.yaml` to set the endpoint that will handle the authentication in behalf of TinShop. -For other type of protection, you can whitelist your own switch and this will do the trick. +In the future, a proper user management will be incorporated into TinShop to handle it. + +In addition, for other type of protection, you can whitelist/blacklist your own switch and this will do the trick. ## I have tons of missing title displayed in `tinfoil`, what should I do? diff --git a/config.example.yaml b/config.example.yaml index 946aac6..55f81dd 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -62,6 +62,12 @@ security: # You can find the uid of a switch in the log upon access backlist: - NOACCESSNOACCESSNOACCESSNOACCESSNOACCESSNOACCESSNOACCESSNOACCESS + # Endpoint to which a query will be sent to verify user/password/uid to + # Headers sent : + # - Authorization: same as sent by switch + # - Device-Id: Switch fingerprint + # Response with status code other than 200 will be treated as failure + forwardAuth: https://auth.tinshop.com/switch # This section describe all custom title db to show up properly in tinfoil customTitledb: diff --git a/config/config.go b/config/config.go index 2d91a36..664ef14 100644 --- a/config/config.go +++ b/config/config.go @@ -28,6 +28,7 @@ type security struct { Whitelist []string `mapstructure:"whitelist"` Backlist []string `mapstructure:"backlist"` BannedTheme []string `mapstructure:"bannedTheme"` + ForwardAuth string `mapstructure:"forwardAuth"` } type nsp struct { @@ -261,6 +262,11 @@ func (cfg *File) VerifyNSP() bool { return cfg.NSP.CheckVerified } +// ForwardAuthURL returns the url of the forward auth +func (cfg *File) ForwardAuthURL() string { + return cfg.Security.ForwardAuth +} + // IsBlacklisted tells if the uid is blacklisted or not func (cfg *File) IsBlacklisted(uid string) bool { if len(cfg.Security.Whitelist) != 0 { diff --git a/mock_repository/mock_config.go b/mock_repository/mock_config.go index ee9507c..9bfbff1 100644 --- a/mock_repository/mock_config.go +++ b/mock_repository/mock_config.go @@ -142,6 +142,20 @@ func (mr *MockConfigMockRecorder) Directories() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Directories", reflect.TypeOf((*MockConfig)(nil).Directories)) } +// ForwardAuthURL mocks base method. +func (m *MockConfig) ForwardAuthURL() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ForwardAuthURL") + ret0, _ := ret[0].(string) + return ret0 +} + +// ForwardAuthURL indicates an expected call of ForwardAuthURL. +func (mr *MockConfigMockRecorder) ForwardAuthURL() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForwardAuthURL", reflect.TypeOf((*MockConfig)(nil).ForwardAuthURL)) +} + // Host mocks base method. func (m *MockConfig) Host() string { m.ctrl.T.Helper() diff --git a/repository/interfaces.go b/repository/interfaces.go index c38a34d..60cbaf2 100644 --- a/repository/interfaces.go +++ b/repository/interfaces.go @@ -45,6 +45,7 @@ type Config interface { ShopTemplateData() ShopTemplate SetShopTemplateData(ShopTemplate) + ForwardAuthURL() string IsBlacklisted(string) bool IsWhitelisted(string) bool IsBannedTheme(string) bool diff --git a/security.go b/security.go index 0c32cf9..5fed95a 100644 --- a/security.go +++ b/security.go @@ -47,6 +47,8 @@ func (s *TinShop) TinfoilMiddleware(next http.Handler) http.Handler { return } + // TODO: Here implement usage of IsWhitelisted + // Check for banned theme var theme = strings.Join(headers["Theme"], "") if s.Shop.Config.IsBannedTheme(theme) { @@ -72,6 +74,26 @@ func (s *TinShop) TinfoilMiddleware(next http.Handler) http.Handler { // Enforce true tinfoil queries // TODO: Check Uauth and Hauth headers log.Printf("Switch %s, %s, %s, %s, %s, %s requesting %s", headers["Theme"], headers["Uid"], headers["Version"], headers["Language"], headers["Hauth"], headers["Uauth"], r.RequestURI) + + // Check user password + if s.Shop.Config.ForwardAuthURL() != "" && headers["Authorization"] != nil { + log.Println("[Security] Forwarding auth to", s.Shop.Config.ForwardAuthURL()) + client := &http.Client{} + req, _ := http.NewRequest("GET", s.Shop.Config.ForwardAuthURL(), nil) + req.Header.Add("Authorization", strings.Join(headers["Authorization"], "")) + req.Header.Add("Device-Id", strings.Join(headers["Uid"], "")) + resp, err := client.Do(req) + if err != nil { + log.Print(err) + _ = shopTemplate.Execute(w, s.Shop.Config.ShopTemplateData()) + return + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + _ = shopTemplate.Execute(w, s.Shop.Config.ShopTemplateData()) + return + } + } } // Call the next handler, which can be another middleware in the chain, or the final handler. diff --git a/security_test.go b/security_test.go index ab178a1..2e1ba0a 100644 --- a/security_test.go +++ b/security_test.go @@ -106,6 +106,11 @@ var _ = Describe("Security", func() { Return(false). AnyTimes() + myMockConfig.EXPECT(). + ForwardAuthURL(). + Return(""). + AnyTimes() + myMockConfig.EXPECT(). IsBlacklisted(gomock.Any()). Return(true). @@ -160,6 +165,11 @@ var _ = Describe("Security", func() { Return(false). AnyTimes() + myMockConfig.EXPECT(). + ForwardAuthURL(). + Return(""). + AnyTimes() + myMockConfig.EXPECT(). IsBlacklisted(gomock.Any()). Return(false). @@ -219,6 +229,11 @@ var _ = Describe("Security", func() { Return(false). AnyTimes() + myMockConfig.EXPECT(). + ForwardAuthURL(). + Return(""). + AnyTimes() + myMockConfig.EXPECT(). IsBlacklisted(gomock.Any()). Return(false). @@ -279,6 +294,11 @@ var _ = Describe("Security", func() { Return(false). AnyTimes() + myMockConfig.EXPECT(). + ForwardAuthURL(). + Return(""). + AnyTimes() + myMockConfig.EXPECT(). IsBlacklisted(gomock.Any()). Return(false). @@ -338,6 +358,11 @@ var _ = Describe("Security", func() { Return(false). AnyTimes() + myMockConfig.EXPECT(). + ForwardAuthURL(). + Return(""). + AnyTimes() + myMockConfig.EXPECT(). IsBlacklisted(gomock.Any()). Return(false).