├── .gitignore ├── LICENSE ├── cmd ├── authsvc │ └── main.go ├── dnasvc │ ├── auth_client.go │ └── main.go └── monolith │ └── main.go └── pkg ├── auth ├── http_server.go ├── repository.go ├── repository_test.go ├── service.go ├── service_test.go └── testdata │ └── fixture.db └── dna ├── http_server.go ├── repository.go ├── repository_test.go ├── service.go ├── service_test.go └── testdata └── fixture.db /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /cmd/authsvc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/go-kit/kit/log" 13 | "github.com/oklog/run" 14 | "github.com/peterbourgon/gattaca/pkg/auth" 15 | "github.com/peterbourgon/usage" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | func main() { 20 | fs := flag.NewFlagSet("authsvc", flag.ExitOnError) 21 | var ( 22 | apiAddr = fs.String("api", "127.0.0.1:8081", "HTTP API listen address") 23 | urn = fs.String("urn", "auth.db", "URN for auth DB") 24 | ) 25 | fs.Usage = usage.For(fs, "authsvc [flags]") 26 | fs.Parse(os.Args[1:]) 27 | 28 | var logger log.Logger 29 | { 30 | logger = log.NewLogfmtLogger(os.Stdout) 31 | logger = log.With(logger, "ts", log.DefaultTimestampUTC) 32 | } 33 | 34 | var authrepo auth.Repository 35 | { 36 | var err error 37 | authrepo, err = auth.NewSQLiteRepository(*urn) 38 | if err != nil { 39 | logger.Log("during", "auth.NewSQLiteRepository", "err", err) 40 | os.Exit(1) 41 | } 42 | } 43 | 44 | var authsvc auth.Service 45 | { 46 | authsvc = auth.NewDefaultService(authrepo) 47 | } 48 | 49 | var api http.Handler 50 | { 51 | api = auth.NewHTTPServer(authsvc) 52 | } 53 | 54 | var g run.Group 55 | { 56 | server := &http.Server{ 57 | Addr: *apiAddr, 58 | Handler: api, 59 | } 60 | g.Add(func() error { 61 | logger.Log("component", "API", "addr", *apiAddr) 62 | return server.ListenAndServe() 63 | }, func(error) { 64 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 65 | defer cancel() 66 | server.Shutdown(ctx) 67 | }) 68 | } 69 | { 70 | ctx, cancel := context.WithCancel(context.Background()) 71 | g.Add(func() error { 72 | c := make(chan os.Signal, 1) 73 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 74 | select { 75 | case <-ctx.Done(): 76 | return ctx.Err() 77 | case sig := <-c: 78 | return errors.Errorf("received signal %s", sig) 79 | } 80 | }, func(error) { 81 | cancel() 82 | }) 83 | } 84 | logger.Log("exit", g.Run()) 85 | } 86 | -------------------------------------------------------------------------------- /cmd/dnasvc/auth_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/peterbourgon/gattaca/pkg/dna" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func newAuthClient(addr string) dna.Validator { 13 | return authClient(addr) 14 | } 15 | 16 | type authClient string // base URL 17 | 18 | func (c authClient) Validate(ctx context.Context, user, token string) error { 19 | url := fmt.Sprintf("%s/validate?user=%s&token=%s", c, user, token) 20 | req, err := http.NewRequest("GET", url, nil) 21 | if err != nil { 22 | return errors.Wrap(err, "error constructing validate request") 23 | } 24 | resp, err := http.DefaultClient.Do(req) 25 | if err != nil { 26 | return errors.Wrap(err, "error making validate request") 27 | } 28 | resp.Body.Close() 29 | if resp.StatusCode != http.StatusOK { 30 | return errors.Errorf("authsvc returned %s", resp.Status) 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /cmd/dnasvc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/go-kit/kit/log" 13 | "github.com/oklog/run" 14 | "github.com/peterbourgon/gattaca/pkg/dna" 15 | "github.com/peterbourgon/usage" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | func main() { 20 | fs := flag.NewFlagSet("dnasvc", flag.ExitOnError) 21 | var ( 22 | apiAddr = fs.String("api", "127.0.0.1:8082", "HTTP API listen address") 23 | urn = fs.String("urn", "dna.db", "URN for DNA DB") 24 | authsvcAddr = fs.String("authsvc", "http://127.0.0.1:8081", "HTTP endpoint for authsvc") 25 | ) 26 | fs.Usage = usage.For(fs, "dnasvc [flags]") 27 | fs.Parse(os.Args[1:]) 28 | 29 | var logger log.Logger 30 | { 31 | logger = log.NewLogfmtLogger(os.Stdout) 32 | logger = log.With(logger, "ts", log.DefaultTimestampUTC) 33 | } 34 | 35 | var dnarepo dna.Repository 36 | { 37 | var err error 38 | dnarepo, err = dna.NewSQLiteRepository(*urn) 39 | if err != nil { 40 | logger.Log("during", "dna.NewSQLiteRepository", "err", err) 41 | os.Exit(1) 42 | } 43 | } 44 | 45 | var validator dna.Validator 46 | { 47 | validator = authClient(*authsvcAddr) 48 | } 49 | 50 | var dnasvc dna.Service 51 | { 52 | dnasvc = dna.NewDefaultService(dnarepo, validator) 53 | } 54 | 55 | var api http.Handler 56 | { 57 | api = dna.NewHTTPServer(dnasvc) 58 | } 59 | 60 | var g run.Group 61 | { 62 | server := &http.Server{ 63 | Addr: *apiAddr, 64 | Handler: api, 65 | } 66 | g.Add(func() error { 67 | logger.Log("component", "API", "addr", *apiAddr) 68 | return server.ListenAndServe() 69 | }, func(error) { 70 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 71 | defer cancel() 72 | server.Shutdown(ctx) 73 | }) 74 | } 75 | { 76 | ctx, cancel := context.WithCancel(context.Background()) 77 | g.Add(func() error { 78 | c := make(chan os.Signal, 1) 79 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 80 | select { 81 | case <-ctx.Done(): 82 | return ctx.Err() 83 | case sig := <-c: 84 | return errors.Errorf("received signal %s", sig) 85 | } 86 | }, func(error) { 87 | cancel() 88 | }) 89 | } 90 | logger.Log("exit", g.Run()) 91 | } 92 | -------------------------------------------------------------------------------- /cmd/monolith/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/go-kit/kit/log" 13 | "github.com/gorilla/mux" 14 | "github.com/oklog/run" 15 | "github.com/peterbourgon/gattaca/pkg/auth" 16 | "github.com/peterbourgon/gattaca/pkg/dna" 17 | "github.com/peterbourgon/usage" 18 | "github.com/pkg/errors" 19 | ) 20 | 21 | func main() { 22 | fs := flag.NewFlagSet("monolith", flag.ExitOnError) 23 | var ( 24 | apiAddr = fs.String("api", "127.0.0.1:8080", "HTTP API listen address") 25 | authURN = fs.String("auth-urn", "file:auth.db", "URN for auth DB") 26 | dnaURN = fs.String("dna-urn", "file:dna.db", "URN for DNA DB") 27 | ) 28 | fs.Usage = usage.For(fs, "monolith [flags]") 29 | fs.Parse(os.Args[1:]) 30 | 31 | var logger log.Logger 32 | { 33 | logger = log.NewLogfmtLogger(os.Stdout) 34 | logger = log.With(logger, "ts", log.DefaultTimestampUTC) 35 | } 36 | 37 | var authsvc auth.Service 38 | { 39 | authrepo, err := auth.NewSQLiteRepository(*authURN) 40 | if err != nil { 41 | logger.Log("during", "auth.NewSQLiteRepository", "err", err) 42 | os.Exit(1) 43 | } 44 | authsvc = auth.NewDefaultService(authrepo) 45 | } 46 | 47 | var authserver http.Handler 48 | { 49 | authserver = auth.NewHTTPServer(authsvc) 50 | } 51 | 52 | var dnasvc dna.Service 53 | { 54 | dnarepo, err := dna.NewSQLiteRepository(*dnaURN) 55 | if err != nil { 56 | logger.Log("during", "dna.NewSQLiteRepository", "err", err) 57 | os.Exit(1) 58 | } 59 | dnasvc = dna.NewDefaultService(dnarepo, authsvc) // don't need a client 60 | } 61 | 62 | var dnaserver http.Handler 63 | { 64 | dnaserver = dna.NewHTTPServer(dnasvc) 65 | } 66 | 67 | var api http.Handler 68 | { 69 | r := mux.NewRouter() 70 | r.PathPrefix("/auth/").Handler(http.StripPrefix("/auth", authserver)) 71 | r.PathPrefix("/dna/").Handler(http.StripPrefix("/dna", dnaserver)) 72 | api = r 73 | } 74 | 75 | var g run.Group 76 | { 77 | server := &http.Server{ 78 | Addr: *apiAddr, 79 | Handler: api, 80 | } 81 | g.Add(func() error { 82 | logger.Log("component", "API", "addr", *apiAddr) 83 | return server.ListenAndServe() 84 | }, func(error) { 85 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 86 | defer cancel() 87 | server.Shutdown(ctx) 88 | }) 89 | } 90 | { 91 | ctx, cancel := context.WithCancel(context.Background()) 92 | g.Add(func() error { 93 | c := make(chan os.Signal, 1) 94 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 95 | select { 96 | case <-ctx.Done(): 97 | return ctx.Err() 98 | case sig := <-c: 99 | return errors.Errorf("received signal %s", sig) 100 | } 101 | }, func(error) { 102 | cancel() 103 | }) 104 | } 105 | logger.Log("exit", g.Run()) 106 | } 107 | -------------------------------------------------------------------------------- /pkg/auth/http_server.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | // HTTPServer wraps a Service and implements http.Handler. 11 | type HTTPServer struct { 12 | router *mux.Router 13 | service Service 14 | } 15 | 16 | // NewHTTPServer returns an HTTPServer wrapping the Service. 17 | func NewHTTPServer(service Service) *HTTPServer { 18 | s := &HTTPServer{ 19 | service: service, 20 | } 21 | r := mux.NewRouter() 22 | { 23 | r.Methods("POST").Path("/signup").HandlerFunc(s.handleSignup) 24 | r.Methods("POST").Path("/login").HandlerFunc(s.handleLogin) 25 | r.Methods("GET").Path("/validate").HandlerFunc(s.handleValidate) 26 | r.Methods("POST").Path("/logout").HandlerFunc(s.handleLogout) 27 | } 28 | s.router = r 29 | return s 30 | } 31 | 32 | // ServeHTTP implements http.Handler, delegating to the mux.Router. 33 | func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 34 | s.router.ServeHTTP(w, r) 35 | } 36 | 37 | func (s *HTTPServer) handleSignup(w http.ResponseWriter, r *http.Request) { 38 | var ( 39 | user = r.URL.Query().Get("user") 40 | pass = r.URL.Query().Get("pass") 41 | ) 42 | if err := s.service.Signup(r.Context(), user, pass); err != nil { 43 | http.Error(w, err.Error(), http.StatusInternalServerError) 44 | return 45 | } 46 | fmt.Fprintln(w, "signup successful") 47 | } 48 | 49 | func (s *HTTPServer) handleLogin(w http.ResponseWriter, r *http.Request) { 50 | var ( 51 | user = r.URL.Query().Get("user") 52 | pass = r.URL.Query().Get("pass") 53 | ) 54 | token, err := s.service.Login(r.Context(), user, pass) 55 | if err == ErrBadAuth { 56 | http.Error(w, err.Error(), http.StatusUnauthorized) 57 | return 58 | } 59 | if err != nil { 60 | http.Error(w, err.Error(), http.StatusInternalServerError) 61 | return 62 | } 63 | fmt.Fprintln(w, token) 64 | } 65 | 66 | func (s *HTTPServer) handleValidate(w http.ResponseWriter, r *http.Request) { 67 | var ( 68 | user = r.URL.Query().Get("user") 69 | token = r.URL.Query().Get("token") 70 | ) 71 | err := s.service.Validate(r.Context(), user, token) 72 | if err != nil { 73 | http.Error(w, err.Error(), http.StatusUnauthorized) 74 | return 75 | } 76 | fmt.Fprintln(w, "validate successful") 77 | } 78 | 79 | func (s *HTTPServer) handleLogout(w http.ResponseWriter, r *http.Request) { 80 | var ( 81 | user = r.URL.Query().Get("user") 82 | token = r.URL.Query().Get("token") 83 | ) 84 | err := s.service.Logout(r.Context(), user, token) 85 | if err == ErrBadAuth { 86 | http.Error(w, err.Error(), http.StatusUnauthorized) 87 | return 88 | } 89 | if err != nil { 90 | http.Error(w, err.Error(), http.StatusInternalServerError) 91 | return 92 | } 93 | fmt.Fprintln(w, "logout successful") 94 | } 95 | -------------------------------------------------------------------------------- /pkg/auth/repository.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "math/rand" 8 | "time" 9 | 10 | _ "github.com/mattn/go-sqlite3" // driver 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // SQLiteRepository for persistence of user credential data. 15 | type SQLiteRepository struct { 16 | db *sql.DB 17 | } 18 | 19 | // NewSQLiteRepository connects to the DB represented by URN. 20 | func NewSQLiteRepository(urn string) (*SQLiteRepository, error) { 21 | db, err := sql.Open("sqlite3", urn) 22 | if err != nil { 23 | return nil, errors.Wrap(err, "error opening DB") 24 | } 25 | 26 | if _, err := db.Query(`SELECT 1 FROM credentials`); err != nil { 27 | if _, err := db.Exec(`CREATE TABLE credentials (user STRING NOT NULL PRIMARY KEY, pass STRING NOT NULL)`); err != nil { 28 | return nil, errors.Wrap(err, "error creating credentials table") 29 | } 30 | for user, pass := range map[string]string{ 31 | "alice": "hunter2", 32 | "bob": "qwerty", 33 | } { 34 | if _, err := db.Exec(`INSERT INTO credentials (user, pass) VALUES (?, ?)`, user, pass); err != nil { 35 | return nil, errors.Wrap(err, "error populating initial credentials") 36 | } 37 | } 38 | } 39 | 40 | if _, err := db.Query(`SELECT 1 FROM tokens`); err != nil { 41 | if _, err := db.Exec(`CREATE TABLE tokens (user STRING NOT NULL PRIMARY KEY, token STRING NOT NULL)`); err != nil { 42 | return nil, errors.Wrap(err, "error creating tokens table") 43 | } 44 | } 45 | 46 | return &SQLiteRepository{ 47 | db: db, 48 | }, nil 49 | } 50 | 51 | // Create a user with associated password. 52 | // The user still needs to log in. 53 | func (r *SQLiteRepository) Create(ctx context.Context, user, pass string) error { 54 | if _, err := r.db.Exec(`INSERT INTO credentials (user, pass) VALUES (?, ?)`, user, pass); err != nil { 55 | return errors.Wrap(err, "error creating user") 56 | } 57 | return nil 58 | } 59 | 60 | // Auth a user, if the pass is correct, and return a token. 61 | // If the user is already authed, overwrites the token. 62 | func (r *SQLiteRepository) Auth(ctx context.Context, user, pass string) (token string, err error) { 63 | tx, err := r.db.BeginTx(ctx, nil) 64 | if err != nil { 65 | return "", errors.Wrap(err, "error starting auth transaction") 66 | } 67 | 68 | defer func() { 69 | if err == nil { 70 | if commitErr := tx.Commit(); commitErr != nil { 71 | err = errors.Wrap(commitErr, "error committing auth transaction") 72 | } 73 | } else { 74 | tx.Rollback() // ignore error 75 | } 76 | }() 77 | 78 | var want string 79 | err = tx.QueryRowContext(ctx, `SELECT pass FROM credentials WHERE user = ?`, user).Scan(&want) 80 | if err == sql.ErrNoRows { 81 | return "", ErrBadAuth 82 | } 83 | if err != nil { 84 | return "", errors.Wrap(err, "error reading credentials from repository") 85 | } 86 | if pass != want { 87 | return "", ErrBadAuth 88 | } 89 | 90 | p := make([]byte, 8) 91 | rand.New(rand.NewSource(time.Now().UnixNano())).Read(p) 92 | token = fmt.Sprintf("%x", p) 93 | if _, err = tx.ExecContext(ctx, `INSERT INTO tokens(user, token) VALUES(?, ?)`, user, token); err != nil { 94 | return "", errors.Wrap(err, "error saving token to repository") 95 | } 96 | 97 | return token, nil 98 | } 99 | 100 | // Deauth a user, if the token is correct. 101 | func (r *SQLiteRepository) Deauth(ctx context.Context, user, token string) error { 102 | tx, err := r.db.BeginTx(ctx, nil) 103 | if err != nil { 104 | return errors.Wrap(err, "error starting deauth transaction") 105 | } 106 | 107 | defer func() { 108 | if err == nil { 109 | if commitErr := tx.Commit(); commitErr != nil { 110 | err = errors.Wrap(commitErr, "error committing deauth transaction") 111 | } 112 | } else { 113 | tx.Rollback() // ignore error 114 | } 115 | }() 116 | 117 | var want string 118 | err = tx.QueryRowContext(ctx, `SELECT token FROM tokens WHERE user = ?`, user).Scan(&want) 119 | if err == sql.ErrNoRows { 120 | return ErrBadAuth // not logged in 121 | } 122 | if err != nil { 123 | return errors.Wrap(err, "error reading token from repository") 124 | } 125 | if token != want { 126 | return ErrBadAuth 127 | } 128 | 129 | if _, err := tx.ExecContext(ctx, `DELETE FROM tokens WHERE user = ?`, user); err != nil { 130 | return errors.Wrap(err, "error removing token from repository") 131 | } 132 | 133 | return nil 134 | } 135 | 136 | // Validate the user and token. 137 | func (r *SQLiteRepository) Validate(ctx context.Context, user, token string) error { 138 | var want string 139 | err := r.db.QueryRowContext(ctx, `SELECT token FROM tokens WHERE user = ?`, user).Scan(&want) 140 | if err == sql.ErrNoRows { 141 | return ErrBadAuth // not logged in 142 | } 143 | if err != nil { 144 | return errors.Wrap(err, "error reading token from repository") 145 | } 146 | if token != want { 147 | return ErrBadAuth 148 | } 149 | 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /pkg/auth/repository_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestSQLiteFixture(t *testing.T) { 10 | r, err := NewSQLiteRepository("file:testdata/fixture.db") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | _, err = r.Auth(context.Background(), "bob", "bad password") 16 | if want, have := ErrBadAuth, err; want != have { 17 | t.Errorf("Auth with bad creds: want %v, have %v", want, have) 18 | } 19 | token, err := r.Auth(context.Background(), "bob", "qwerty") 20 | if want, have := error(nil), err; want != have { 21 | t.Fatalf("Auth failed: %v", err) 22 | } 23 | 24 | if want, have := ErrBadAuth, r.Validate(context.Background(), "bob", "bad token"); want != have { 25 | t.Errorf("Validate with bad token: want %v, have %v", want, have) 26 | } 27 | if want, have := error(nil), r.Validate(context.Background(), "bob", token); want != have { 28 | t.Errorf("Validate: want %v, have %v", want, have) 29 | } 30 | 31 | if want, have := ErrBadAuth, r.Deauth(context.Background(), "bob", "bad token"); want != have { 32 | t.Errorf("Deauth with bad token: want %v, have %v", want, have) 33 | } 34 | if want, have := error(nil), r.Deauth(context.Background(), "bob", token); want != have { 35 | t.Errorf("Deauth: want %v, have %v", want, have) 36 | } 37 | } 38 | 39 | func TestSQLiteIntegration(t *testing.T) { 40 | var ( 41 | filevar = "AUTH_INTEGRATION_TEST_FILE" 42 | filename = os.Getenv(filevar) 43 | ) 44 | if filename == "" { 45 | // If a test will write to disk, make it opt-in. 46 | t.Skipf("skipping; set %s to run this test", filevar) 47 | } 48 | 49 | if _, err := os.Stat(filename); !os.IsNotExist(err) { 50 | t.Fatalf("%s: %v", filename, err) 51 | } 52 | 53 | defer func() { 54 | if err := os.Remove(filename); err != nil { 55 | t.Errorf("rm %s: %v", filename, err) 56 | } 57 | }() 58 | 59 | r, err := NewSQLiteRepository("file:" + filename) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | const ( 65 | user = "alpha" 66 | pass = "beta" 67 | ) 68 | if want, have := error(nil), r.Create(context.Background(), user, pass); want != have { 69 | t.Fatalf("Create: want %v, have %v", want, have) 70 | } 71 | 72 | token, err := r.Auth(context.Background(), user, pass) 73 | if want, have := error(nil), err; want != have { 74 | t.Fatalf("Auth: want %v, have %v", want, have) 75 | } 76 | 77 | if want, have := error(nil), r.Validate(context.Background(), user, token); want != have { 78 | t.Errorf("Validate: want %v, have %v", want, have) 79 | } 80 | 81 | if want, have := error(nil), r.Deauth(context.Background(), user, token); want != have { 82 | t.Errorf("Deauth: want %v, have %v", want, have) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/auth/service.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | // Service describes the expected behavior of the authentication service. 9 | // Users can create accounts, log in, and log out; other services can 10 | // validate user tokens (sessions) that they've received. 11 | type Service interface { 12 | Signup(ctx context.Context, user, pass string) error 13 | Login(ctx context.Context, user, pass string) (token string, err error) 14 | Logout(ctx context.Context, user, token string) error 15 | Validate(ctx context.Context, user, token string) error 16 | } 17 | 18 | // ErrBadAuth is returned when authentication fails for any reason. 19 | var ErrBadAuth = errors.New("bad auth") 20 | 21 | // DefaultService provides authentication via a repository (DB). 22 | // It's a very thin layer around the repository. 23 | type DefaultService struct { 24 | repo Repository 25 | } 26 | 27 | // NewDefaultService returns a usable service, wrapping a repository. 28 | func NewDefaultService(repo Repository) *DefaultService { 29 | return &DefaultService{ 30 | repo: repo, 31 | } 32 | } 33 | 34 | // Signup creates a user with the given pass. 35 | // The user still needs to login. 36 | func (s *DefaultService) Signup(ctx context.Context, user, pass string) (err error) { 37 | return s.repo.Create(ctx, user, pass) 38 | } 39 | 40 | // Login logs the user in, if the pass is correct. 41 | // The returned token should be passed to Logout or Validate. 42 | func (s *DefaultService) Login(ctx context.Context, user, pass string) (token string, err error) { 43 | return s.repo.Auth(ctx, user, pass) 44 | } 45 | 46 | // Logout logs the user out, if the token is valid. 47 | func (s *DefaultService) Logout(ctx context.Context, user, token string) (err error) { 48 | return s.repo.Deauth(ctx, user, token) 49 | } 50 | 51 | // Validate returns a nil error if the user is logged in and 52 | // provides the correct token. 53 | func (s *DefaultService) Validate(ctx context.Context, user, token string) (err error) { 54 | return s.repo.Validate(ctx, user, token) 55 | } 56 | 57 | // Repository models the data access layer required by the auth service. 58 | // It's very similar to the service interface, because authentication 59 | // doesn't involve much business logic. 60 | type Repository interface { 61 | Create(ctx context.Context, user, pass string) error 62 | Auth(ctx context.Context, user, pass string) (token string, err error) 63 | Deauth(ctx context.Context, user, token string) error 64 | Validate(ctx context.Context, user, token string) error 65 | } 66 | -------------------------------------------------------------------------------- /pkg/auth/service_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func TestFlow(t *testing.T) { 14 | s := NewDefaultService(newMockRepo()) 15 | 16 | if want, have := error(nil), s.Signup(context.Background(), "peter", "123456"); want != have { 17 | t.Fatalf("Signup: want %v, have %v", want, have) 18 | } 19 | 20 | token, err := s.Login(context.Background(), "peter", "123456") 21 | if want, have := error(nil), err; want != have { 22 | t.Fatalf("Login: want %v, have %v", want, have) 23 | } 24 | 25 | if want, have := error(nil), s.Validate(context.Background(), "peter", token); want != have { 26 | t.Errorf("Validate: want %v, have %v", want, have) 27 | } 28 | 29 | if want, have := error(nil), s.Logout(context.Background(), "peter", token); want != have { 30 | t.Errorf("Logout: want %v, have %v", want, have) 31 | } 32 | 33 | if want, have := ErrBadAuth, s.Validate(context.Background(), "peter", token); want != have { 34 | t.Errorf("Validate after Logout: want %v, have %v", want, have) 35 | } 36 | } 37 | 38 | type mockRepo struct { 39 | creds map[string]string 40 | tokens map[string]string 41 | } 42 | 43 | func newMockRepo() *mockRepo { 44 | return &mockRepo{ 45 | creds: map[string]string{}, 46 | tokens: map[string]string{}, 47 | } 48 | } 49 | 50 | func (r *mockRepo) Create(ctx context.Context, user, pass string) error { 51 | if _, ok := r.creds[user]; ok { 52 | return errors.New("user already exists") 53 | } 54 | 55 | r.creds[user] = pass 56 | return nil 57 | } 58 | 59 | func (r *mockRepo) Auth(ctx context.Context, user, pass string) (token string, err error) { 60 | if have, ok := r.creds[user]; !ok || pass != have { 61 | return "", ErrBadAuth 62 | } 63 | 64 | p := make([]byte, 8) 65 | rand.New(rand.NewSource(time.Now().UnixNano())).Read(p) 66 | token = fmt.Sprintf("%x", p) 67 | r.tokens[user] = token 68 | return token, nil 69 | } 70 | 71 | func (r *mockRepo) Deauth(ctx context.Context, user, token string) error { 72 | if have, ok := r.tokens[user]; !ok || token != have { 73 | return ErrBadAuth 74 | } 75 | delete(r.tokens, user) 76 | return nil 77 | } 78 | 79 | func (r *mockRepo) Validate(ctx context.Context, user, token string) error { 80 | if have, ok := r.tokens[user]; !ok || token != have { 81 | return ErrBadAuth 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/auth/testdata/fixture.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbourgon/gattaca/a012da003cb715aa6327416f6c0fa29baac2f49d/pkg/auth/testdata/fixture.db -------------------------------------------------------------------------------- /pkg/dna/http_server.go: -------------------------------------------------------------------------------- 1 | package dna 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // HTTPServer wraps a Service and implements http.Handler. 10 | type HTTPServer struct { 11 | service Service 12 | } 13 | 14 | // NewHTTPServer returns an HTTPServer wrapping the Service. 15 | func NewHTTPServer(service Service) *HTTPServer { 16 | return &HTTPServer{ 17 | service: service, 18 | } 19 | } 20 | 21 | // ServeHTTP implements http.Handler. 22 | func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 23 | var ( 24 | first = extractPathToken(r.URL.Path, 0) 25 | method = r.Method 26 | ) 27 | switch { 28 | case method == "POST" && first == "add": 29 | var ( 30 | user = r.URL.Query().Get("user") 31 | token = r.URL.Query().Get("token") 32 | sequence = r.URL.Query().Get("sequence") 33 | ) 34 | err := s.service.Add(r.Context(), user, token, sequence) 35 | switch { 36 | case err == nil: 37 | fmt.Fprintln(w, "Add OK") 38 | case err == ErrBadAuth: 39 | http.Error(w, err.Error(), http.StatusUnauthorized) 40 | case err != nil: 41 | http.Error(w, err.Error(), http.StatusInternalServerError) 42 | } 43 | 44 | case method == "GET" && first == "check": 45 | var ( 46 | user = r.URL.Query().Get("user") 47 | token = r.URL.Query().Get("token") 48 | subsequence = r.URL.Query().Get("subsequence") 49 | ) 50 | err := s.service.Check(r.Context(), user, token, subsequence) 51 | switch { 52 | case err == nil: 53 | fmt.Fprintln(w, "Subsequence found") 54 | case err == ErrSubsequenceNotFound: 55 | http.Error(w, err.Error(), http.StatusNotFound) 56 | case err == ErrBadAuth: 57 | http.Error(w, err.Error(), http.StatusUnauthorized) 58 | default: 59 | http.Error(w, err.Error(), http.StatusInternalServerError) 60 | } 61 | 62 | default: 63 | http.NotFound(w, r) 64 | } 65 | } 66 | 67 | func extractPathToken(path string, position int) string { 68 | toks := strings.Split(strings.Trim(path, "/ "), "/") 69 | if len(toks) <= position { 70 | return "" 71 | } 72 | return toks[position] 73 | } 74 | -------------------------------------------------------------------------------- /pkg/dna/repository.go: -------------------------------------------------------------------------------- 1 | package dna 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | _ "github.com/mattn/go-sqlite3" // driver 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // ErrInvalidUser is returned when an invalid user is passed to Select. 12 | var ErrInvalidUser = errors.New("invalid user") 13 | 14 | // SQLiteRepository for persistence of the DNA sequences. 15 | type SQLiteRepository struct { 16 | db *sql.DB 17 | } 18 | 19 | // NewSQLiteRepository connects to the DB represented by URN. 20 | func NewSQLiteRepository(urn string) (*SQLiteRepository, error) { 21 | db, err := sql.Open("sqlite3", urn) 22 | if err != nil { 23 | return nil, errors.Wrap(err, "error opening DB") 24 | } 25 | 26 | if _, err := db.Query(`SELECT 1 FROM dna`); err != nil { 27 | if _, err := db.Exec(`CREATE TABLE dna (user STRING NOT NULL PRIMARY KEY, sequence STRING NOT NULL)`); err != nil { 28 | return nil, errors.Wrap(err, "error creating dna table") 29 | } 30 | for user, sequence := range map[string]string{ 31 | "alice": "attcgtattattttttgatatttttccacaaaaatacagactaaatacaactgaatacag", 32 | "bob": "tgcaaaattagatataaatgtaaacgaacataaaaacttttataagacaggattaagtta", 33 | } { 34 | if _, err := db.Exec(`INSERT INTO dna (user, sequence) VALUES (?, ?)`, user, sequence); err != nil { 35 | return nil, errors.Wrap(err, "error populating initial sequences") 36 | } 37 | } 38 | } 39 | 40 | return &SQLiteRepository{ 41 | db: db, 42 | }, nil 43 | } 44 | 45 | // Insert a user's DNA sequence to the repository. 46 | func (r *SQLiteRepository) Insert(ctx context.Context, user, sequence string) error { 47 | _, err := r.db.ExecContext(ctx, `INSERT INTO dna(user, sequence) VALUES(?, ?)`, user, sequence) 48 | if err != nil { 49 | return errors.Wrap(err, "error writing to repository") 50 | } 51 | return nil 52 | } 53 | 54 | // Select a user's DNA sequence from the repository. 55 | func (r *SQLiteRepository) Select(ctx context.Context, user string) (sequence string, err error) { 56 | if err := r.db.QueryRowContext(ctx, `SELECT sequence FROM dna WHERE user = ?`, user).Scan(&sequence); err == sql.ErrNoRows { 57 | return "", ErrInvalidUser 58 | } else if err != nil { 59 | return "", errors.Wrap(err, "error reading from repository") 60 | } 61 | return sequence, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/dna/repository_test.go: -------------------------------------------------------------------------------- 1 | package dna 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestSQLiteFixture(t *testing.T) { 10 | r, err := NewSQLiteRepository("file:testdata/fixture.db") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | _, err = r.Select(context.Background(), "invalid user") 16 | if want, have := ErrInvalidUser, err; want != have { 17 | t.Errorf("Select with bad user: want %v, have %v", want, have) 18 | } 19 | 20 | sequence, err := r.Select(context.Background(), "charlie") 21 | if want, have := error(nil), err; want != have { 22 | t.Errorf("Select with bad user: want %v, have %v", want, have) 23 | } 24 | if want, have := "aaaggactgcgcgccagttaagccctgttgtt", sequence; want != have { 25 | t.Errorf("Select sequence: want %q, have %q", want, have) 26 | } 27 | } 28 | 29 | func TestSQLiteIntegration(t *testing.T) { 30 | var ( 31 | filevar = "DNA_INTEGRATION_TEST_FILE" 32 | filename = os.Getenv(filevar) 33 | ) 34 | if filename == "" { 35 | // If a test will write to disk, make it opt-in. 36 | t.Skipf("skipping; set %s to run this test", filevar) 37 | } 38 | 39 | if _, err := os.Stat(filename); !os.IsNotExist(err) { 40 | t.Fatalf("%s: %v", filename, err) 41 | } 42 | 43 | defer func() { 44 | if err := os.Remove(filename); err != nil { 45 | t.Errorf("rm %s: %v", filename, err) 46 | } 47 | }() 48 | 49 | r, err := NewSQLiteRepository("file:" + filename) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | var ( 55 | user = "vincent" 56 | sequence = "gattaca" 57 | ) 58 | if want, have := error(nil), r.Insert(context.Background(), user, sequence); want != have { 59 | t.Fatalf("Insert: want %v, have %v", want, have) 60 | } 61 | 62 | selected, err := r.Select(context.Background(), user) 63 | if want, have := error(nil), err; want != have { 64 | t.Fatalf("Select: want %v, have %v", want, have) 65 | } 66 | if want, have := sequence, selected; want != have { 67 | t.Fatalf("Select: want %v, have %v", want, have) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/dna/service.go: -------------------------------------------------------------------------------- 1 | package dna 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Service describes the expected behavior of the DNA sequence service. 11 | // Users can add their DNA, and check if subsequences exist. 12 | type Service interface { 13 | Add(ctx context.Context, user, token, sequence string) error 14 | Check(ctx context.Context, user, token, subsequence string) error 15 | } 16 | 17 | // DefaultService provides our DNA sequence business logic. 18 | type DefaultService struct { 19 | repo Repository 20 | valid Validator 21 | } 22 | 23 | var ( 24 | // ErrSubsequenceNotFound is returned by Check on a failure. 25 | ErrSubsequenceNotFound = errors.New("subsequence doesn't appear in the DNA sequence") 26 | 27 | // ErrBadAuth is returned if a user validation check fails. 28 | ErrBadAuth = errors.New("bad auth") 29 | 30 | // ErrInvalidSequence is returned if an invalid sequence is added. 31 | ErrInvalidSequence = errors.New("invalid DNA sequence") 32 | ) 33 | 34 | // Repository is a client-side interface, which models 35 | // the concrete e.g. SQLiteRepository. 36 | type Repository interface { 37 | Insert(ctx context.Context, user, sequence string) error 38 | Select(ctx context.Context, user string) (sequence string, err error) 39 | } 40 | 41 | // Validator is a client-side interface, which models 42 | // the parts of the auth service that we use. 43 | type Validator interface { 44 | Validate(ctx context.Context, user, token string) error 45 | } 46 | 47 | // NewDefaultService returns a usable service, wrapping a repository. 48 | func NewDefaultService(r Repository, v Validator) *DefaultService { 49 | return &DefaultService{ 50 | repo: r, 51 | valid: v, 52 | } 53 | } 54 | 55 | // Add a user and their DNA sequence to the database. 56 | func (s *DefaultService) Add(ctx context.Context, user, token, sequence string) (err error) { 57 | if err := s.valid.Validate(ctx, user, token); err != nil { 58 | return ErrBadAuth 59 | } 60 | 61 | if !validSequence(sequence) { 62 | return ErrInvalidSequence 63 | } 64 | 65 | if err := s.repo.Insert(ctx, user, sequence); err != nil { 66 | return errors.Wrap(err, "error adding new user") 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // Check returns true if the given subsequence is present in the user's DNA. 73 | func (s *DefaultService) Check(ctx context.Context, user, token, subsequence string) (err error) { 74 | if err := s.valid.Validate(ctx, user, token); err != nil { 75 | return ErrBadAuth 76 | } 77 | 78 | sequence, err := s.repo.Select(ctx, user) 79 | if err != nil { 80 | return errors.Wrap(err, "error reading DNA sequence from repository") 81 | } 82 | 83 | if !strings.Contains(sequence, subsequence) { 84 | return ErrSubsequenceNotFound 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func validSequence(sequence string) bool { 91 | for _, r := range sequence { 92 | switch r { 93 | case 'g', 'a', 't', 'c': 94 | continue 95 | default: 96 | return false 97 | } 98 | } 99 | return true 100 | } 101 | -------------------------------------------------------------------------------- /pkg/dna/service_test.go: -------------------------------------------------------------------------------- 1 | package dna 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func TestFlow(t *testing.T) { 11 | var ( 12 | repo = newMockRepo() 13 | user = "vincent" 14 | token = "some_token" 15 | valid = newMockValidator(user, token) 16 | s = NewDefaultService(repo, valid) 17 | ) 18 | 19 | if want, have := ErrBadAuth, s.Add(context.Background(), user, "invalid_token", "gattaca"); want != have { 20 | t.Errorf("Add with bad token: want %v, have %v", want, have) 21 | } 22 | if want, have := error(nil), s.Add(context.Background(), "vincent", "some_token", "gattaca"); want != have { 23 | t.Errorf("Add: want %v, have %v", want, have) 24 | } 25 | 26 | for subsequence, want := range map[string]error{ 27 | "": nil, 28 | "g": nil, 29 | "ga": nil, 30 | "gattac": nil, 31 | "gattaca": nil, 32 | "x": ErrSubsequenceNotFound, 33 | "gata": ErrSubsequenceNotFound, 34 | "gattacaa": ErrSubsequenceNotFound, 35 | } { 36 | if have := s.Check(context.Background(), "vincent", "some_token", subsequence); want != have { 37 | t.Errorf("Check(%q): want %v, have %v", subsequence, want, have) 38 | } 39 | } 40 | } 41 | 42 | func TestValidSequences(t *testing.T) { 43 | for sequence, want := range map[string]error{ 44 | "": nil, 45 | "gattaca": nil, 46 | "abba": ErrInvalidSequence, 47 | "metallica": ErrInvalidSequence, 48 | } { 49 | var ( 50 | repo = newMockRepo() 51 | user = "foo" 52 | token = "bar" 53 | valid = newMockValidator(user, token) 54 | s = NewDefaultService(repo, valid) 55 | ) 56 | if have := s.Add(context.Background(), user, token, sequence); want != have { 57 | t.Errorf("Add(%q): want %v, have %v", sequence, want, have) 58 | } 59 | } 60 | } 61 | 62 | type mockRepo struct { 63 | dna map[string]string 64 | } 65 | 66 | func newMockRepo() *mockRepo { 67 | return &mockRepo{ 68 | dna: map[string]string{}, 69 | } 70 | } 71 | 72 | func (r *mockRepo) Insert(ctx context.Context, user, sequence string) error { 73 | if _, ok := r.dna[user]; ok { 74 | return errors.New("user already exists") 75 | } 76 | 77 | r.dna[user] = sequence 78 | return nil 79 | } 80 | 81 | func (r *mockRepo) Select(ctx context.Context, user string) (sequence string, err error) { 82 | sequence, ok := r.dna[user] 83 | if !ok { 84 | return "", ErrInvalidUser 85 | } 86 | return sequence, nil 87 | } 88 | 89 | type mockValidator struct { 90 | tokens map[string]string 91 | } 92 | 93 | func newMockValidator(usertokens ...string) *mockValidator { 94 | tokens := map[string]string{} 95 | for i := 0; i < len(usertokens); i += 2 { 96 | tokens[usertokens[i]] = usertokens[i+1] 97 | } 98 | return &mockValidator{ 99 | tokens: tokens, 100 | } 101 | } 102 | 103 | func (v *mockValidator) Validate(ctx context.Context, user, token string) error { 104 | if have, ok := v.tokens[user]; !ok || token != have { 105 | return ErrBadAuth 106 | } 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /pkg/dna/testdata/fixture.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbourgon/gattaca/a012da003cb715aa6327416f6c0fa29baac2f49d/pkg/dna/testdata/fixture.db --------------------------------------------------------------------------------