├── api ├── swagger-ui │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── swagger-ui.css.map │ ├── index.html │ └── oauth2-redirect.html └── StaticMiddleware.go ├── .docker └── Dockerfile ├── .idea ├── misc.xml ├── vcs.xml ├── modules.xml ├── hansip.iml └── watcherTasks.xml ├── internal ├── passphrase │ ├── Words_test.go │ ├── Passphrase.go │ └── Passphrase_test.go ├── endpoint │ ├── Static.go │ ├── CorsOptionMiddleware.go │ ├── UserManagement_test.go │ ├── ClientIpResolverMiddleware.go │ ├── TransactionIdMiddleware.go │ ├── Endpoint_test.go │ ├── JwtMiddleware.go │ ├── Recovery.go │ ├── Endpoint.go │ ├── TenantManagement.go │ ├── Management.go │ └── Authentication.go ├── constants │ └── Constants.go ├── connector │ ├── MySqlDbConnector_test.go │ ├── DbErrors.go │ ├── hansip.sql │ ├── EmailSendConnector.go │ └── DataAccessObjects.go ├── hansipcontext │ └── HansipContext.go ├── hansiperrors │ └── HansipErrors.go ├── gzip │ └── GzipEncodingMiddleware.go ├── mailer │ └── Mailer.go ├── config │ └── Configuration.go └── server │ └── Server.go ├── .vscode └── launch.json ├── cmd └── main │ └── Main.go ├── LICENSE.txt ├── go.mod ├── pkg ├── helper │ ├── RandomString.go │ ├── HealthCheck.go │ ├── StringHelper.go │ ├── TokenFactory_test.go │ ├── HttpHelper.go │ ├── Pagination.go │ ├── RoleUtil.go │ ├── RoleUtil_test.go │ ├── role-check.php │ ├── TokenFactory.go │ ├── Pagination_test.go │ └── DoubleStar.go ├── totp │ ├── Secret.go │ └── Otp.go └── store │ └── cache │ ├── ObjectCache_test.go │ └── ObjectCache.go ├── CONTRIBUTING.md ├── Makefile ├── CODE_OF_CONDUCTS.md ├── .gitignore ├── README.md └── LICENSE-2.0.txt /api/swagger-ui/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperjumptech/hansip/HEAD/api/swagger-ui/favicon-16x16.png -------------------------------------------------------------------------------- /api/swagger-ui/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperjumptech/hansip/HEAD/api/swagger-ui/favicon-32x32.png -------------------------------------------------------------------------------- /api/swagger-ui/swagger-ui.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"swagger-ui.css","sourceRoot":""} -------------------------------------------------------------------------------- /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 2 | ENV GOPATH /go 3 | ENV GO111MODULE on 4 | ENV GOOS linux 5 | ENV GOARCH amd64 6 | 7 | RUN go get -v github.com/rubenv/sql-migrate/... 8 | RUN sql-migrate --help 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /internal/passphrase/Words_test.go: -------------------------------------------------------------------------------- 1 | package passphrase 2 | 3 | import "testing" 4 | 5 | func TestRandomPassphrase(t *testing.T) { 6 | enGen := NewEnglishPassphraseGenerator() 7 | for i := 2; i <= 10; i++ { 8 | w, err := enGen.RandomPassphrase(i, 4) 9 | if err != nil { 10 | t.Fail() 11 | } 12 | t.Logf("%d : %s", i, w) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.idea/hansip.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /internal/endpoint/Static.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hyperjumptech/hansip/pkg/helper" 7 | ) 8 | 9 | // HealthCheck serve health check request 10 | func HealthCheck(w http.ResponseWriter, r *http.Request) { 11 | w.Header().Add("cache-control", "no-cache") 12 | w.Header().Add("Content-Type", "application/json") 13 | w.WriteHeader(http.StatusOK) 14 | hc := &helper.HealthCheck{} 15 | _, _ = w.Write([]byte(hc.String())) 16 | } 17 | -------------------------------------------------------------------------------- /internal/endpoint/CorsOptionMiddleware.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // CorsMiddleware intercept request and allow OPTION methods 9 | func CorsMiddleware(next http.Handler) http.Handler { 10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | if strings.ToUpper(r.Method) == http.MethodOptions { 12 | w.WriteHeader(http.StatusOK) 13 | return 14 | } 15 | next.ServeHTTP(w, r) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /internal/constants/Constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // ContextKey a context key to be used with context.Context 4 | type ContextKey int 5 | 6 | const ( 7 | // RequestID is context key for RequestID tracking 8 | RequestID ContextKey = 1 9 | 10 | // HansipAuthentication is context key for hansip authentication information 11 | HansipAuthentication ContextKey = 2 12 | 13 | // RequestIDHeader is context key for tracking request 14 | RequestIDHeader = "X-Request-ID" 15 | ) 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch file", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "debug", 12 | "program": "cmd/main/Main.go" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /cmd/main/Main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hyperjumptech/hansip/internal/server" 6 | ) 7 | 8 | func main() { 9 | fmt.Println( 10 | ` __ __ ____ ____ _____ ____ ____ 11 | | | | / || \ / ___/| || \ 12 | | | || o || _ ( \_ | | | o ) 13 | | _ || || | |\__ | | | | _/ 14 | | | || _ || | |/ \ | | | | | 15 | | | || | || | |\ | | | | | 16 | |__|__||__|__||__|__| \___||____||__| 17 | Access Authentication & Authorization (AAA) server.`) 18 | server.Start() 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 hyperjump.tech 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /internal/passphrase/Passphrase.go: -------------------------------------------------------------------------------- 1 | package passphrase 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // Validate parses your passphrase and validates for minimum characters and words 9 | func Validate(passphrase string, minchars, minwords, mincharsinword int) bool { 10 | if len(passphrase) < minchars { 11 | return false 12 | } 13 | regx := regexp.MustCompile(`[ \t\n]+`) 14 | reps := regx.ReplaceAllString(passphrase, " ") 15 | words := strings.Split(reps, " ") 16 | if len(words) < minwords { 17 | return false 18 | } 19 | for _, w := range words { 20 | if len(w) < mincharsinword { 21 | return false 22 | } 23 | } 24 | return true 25 | } 26 | -------------------------------------------------------------------------------- /internal/endpoint/UserManagement_test.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "encoding/json" 5 | "golang.org/x/crypto/bcrypt" 6 | "testing" 7 | ) 8 | 9 | func TestArrayJsonParsing(t *testing.T) { 10 | jsonStr := `["abc","cde","fgh"]` 11 | target := make([]string, 0) 12 | err := json.Unmarshal([]byte(jsonStr), &target) 13 | if err != nil { 14 | t.Logf(err.Error()) 15 | t.Fail() 16 | } 17 | if target[0] != "abc" { 18 | t.Fail() 19 | } 20 | if target[2] != "fgh" { 21 | t.Fail() 22 | } 23 | } 24 | 25 | func TestGenPass(t *testing.T) { 26 | hash, err := bcrypt.GenerateFromPassword([]byte("abcdefg"), 14) 27 | if err != nil { 28 | t.Fail() 29 | } 30 | t.Log(string(hash)) 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hyperjumptech/hansip 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/SermoDigital/jose v0.0.0-20180104203859-803625baeddc 7 | github.com/go-sql-driver/mysql v1.5.0 8 | github.com/gorilla/mux v1.8.0 9 | github.com/hyperjumptech/jiffy v1.0.0 10 | github.com/mattn/go-sqlite3 v1.14.8 // indirect 11 | github.com/rs/cors v1.7.0 12 | github.com/sendgrid/rest v2.6.1+incompatible // indirect 13 | github.com/sendgrid/sendgrid-go v3.6.4+incompatible 14 | github.com/sirupsen/logrus v1.7.0 15 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 16 | github.com/spf13/viper v1.7.1 17 | github.com/stretchr/testify v1.3.0 18 | golang.org/x/crypto v0.0.0-20200930160638-afb6bcd081ae 19 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect 20 | ) 21 | 22 | exclude github.com/SermoDigital/jose v0.9.1 23 | -------------------------------------------------------------------------------- /pkg/helper/RandomString.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "bytes" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | const ( 10 | upper = `ABCDEFGHIJKLMNOPQRSTUVWXYZ` 11 | lower = `abcdefghijklmnopqrstuvwxyz` 12 | number = `123456789` 13 | ) 14 | 15 | // MakeRandomString produces a string contains random character with defined specification. 16 | func MakeRandomString(length int, upperAlphas, lowerAlphas, numbers, space bool) string { 17 | if length == 0 { 18 | return "" 19 | } 20 | poolBuff := bytes.Buffer{} 21 | if upperAlphas { 22 | poolBuff.WriteString(upper) 23 | } 24 | if lowerAlphas { 25 | poolBuff.WriteString(lower) 26 | } 27 | if numbers { 28 | poolBuff.WriteString(number) 29 | } 30 | if space { 31 | poolBuff.WriteString(" ") 32 | } 33 | bpool := poolBuff.Bytes() 34 | buff := bytes.Buffer{} 35 | rand.Seed(time.Now().UnixNano()) 36 | for buff.Len() < length { 37 | buff.WriteByte(bpool[rand.Intn(len(bpool))]) 38 | } 39 | return string(buff.Bytes()) 40 | } 41 | -------------------------------------------------------------------------------- /internal/passphrase/Passphrase_test.go: -------------------------------------------------------------------------------- 1 | package passphrase 2 | 3 | import "testing" 4 | 5 | type TestPassphrase struct { 6 | Pass string 7 | MinChar int 8 | MinWord int 9 | MinCharWord int 10 | Valid bool 11 | } 12 | 13 | var ( 14 | testData = []TestPassphrase{ 15 | {"thisisaverylongpass", 10, 1, 1, true}, 16 | {"shortpass", 10, 1, 1, false}, 17 | {"shortpass", 3, 2, 1, false}, 18 | {"short pass", 3, 2, 1, true}, 19 | {"short pass right", 3, 2, 1, true}, 20 | {"short pass good", 3, 2, 4, true}, 21 | {"short pass is good", 3, 2, 4, false}, 22 | } 23 | ) 24 | 25 | func TestValidate(t *testing.T) { 26 | for i, td := range testData { 27 | if Validate(td.Pass, td.MinChar, td.MinWord, td.MinCharWord) != td.Valid { 28 | t.Logf("Test data %d expect %s is minchar %d, minword %d, mincharinword %d to be %v but %v", i, td.Pass, td.MinChar, td.MinWord, td.MinCharWord, td.Valid, Validate(td.Pass, td.MinChar, td.MinWord, td.MinCharWord)) 29 | t.Fail() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/endpoint/ClientIpResolverMiddleware.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | // ForwardedForHeader header key for X-Forwarded-For 10 | ForwardedForHeader = http.CanonicalHeaderKey("X-Forwarded-For") 11 | 12 | // RealIPHeader header key for X-Real-IP 13 | RealIPHeader = http.CanonicalHeaderKey("X-Real-IP") 14 | ) 15 | 16 | // ClientIPResolverMiddleware will try to resolve caller's real IP address by looking for gateway injected header such as X-Forwarded-For and X-Real-IP 17 | func ClientIPResolverMiddleware(next http.Handler) http.Handler { 18 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | ForwardedHeader := r.Header.Get(ForwardedForHeader) 20 | if len(ForwardedForHeader) > 0 { 21 | r.RemoteAddr = ForwardedHeader 22 | } else { 23 | RealHeader := r.Header.Get(RealIPHeader) 24 | if len(RealHeader) > 0 { 25 | i := strings.Index(RealHeader, ", ") 26 | if i == -1 { 27 | i = len(RealHeader) 28 | } 29 | r.RemoteAddr = RealHeader[:i] 30 | } 31 | } 32 | next.ServeHTTP(w, r) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /internal/connector/MySqlDbConnector_test.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | _ "github.com/go-sql-driver/mysql" 7 | "github.com/sirupsen/logrus" 8 | "sort" 9 | "testing" 10 | ) 11 | 12 | func TestSorting(t *testing.T) { 13 | arr := []string{ 14 | "abc", "cde", 15 | } 16 | sort.Slice(arr, func(i, j int) bool { 17 | return i < j 18 | }) 19 | if arr[0] != "abc" { 20 | t.Log(arr[0]) 21 | t.FailNow() 22 | } 23 | } 24 | 25 | func TestUpdateuser(t *testing.T) { 26 | logrus.SetLevel(logrus.TraceLevel) 27 | 28 | db, err := sql.Open("mysql", "devuser:devpassword@/devdb?parseTime=true") 29 | if err != nil { 30 | t.Log(err.Error()) 31 | t.FailNow() 32 | } 33 | 34 | if db == nil { 35 | t.Log("DB nill") 36 | t.FailNow() 37 | } 38 | 39 | mysqldb := &MySQLDB{instance: db} 40 | 41 | ctx := context.Background() 42 | 43 | user, err := mysqldb.GetUserByEmail(ctx, "ferdinand.neman@gmail.com") 44 | if err != nil { 45 | t.FailNow() 46 | } 47 | 48 | //user.Enabled = true 49 | user.FailCount = 1 50 | 51 | err = mysqldb.UpdateUser(ctx, user) 52 | if err != nil { 53 | t.Fail() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/hansipcontext/HansipContext.go: -------------------------------------------------------------------------------- 1 | package hansipcontext 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hyperjumptech/hansip/internal/config" 6 | "strings" 7 | ) 8 | 9 | // AuthenticationContext is a context value to be add into request context. 10 | type AuthenticationContext struct { 11 | Token string 12 | Subject string 13 | Audience []string 14 | TokenType string 15 | } 16 | 17 | // IsAdminOfDomain validate if the user have an admin account of a domain 18 | func (c *AuthenticationContext) IsAdminOfDomain(domain string) bool { 19 | lookFor := fmt.Sprintf("%s@%s", config.Get("hansip.admin"), domain) 20 | hansipRole := fmt.Sprintf("%s@%s", config.Get("hansip.admin"), config.Get("hansip.domain")) 21 | for _, aud := range c.Audience { 22 | if aud == lookFor || aud == hansipRole { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | 29 | // IsAdminOfDomain validate if the user have an admin account of a domain 30 | func (c *AuthenticationContext) IsAnAdmin() bool { 31 | lookFor := fmt.Sprintf("%s@", config.Get("hansip.admin")) 32 | for _, aud := range c.Audience { 33 | if strings.HasPrefix(aud, lookFor) { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | -------------------------------------------------------------------------------- /internal/endpoint/TransactionIdMiddleware.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "context" 5 | "github.com/hyperjumptech/hansip/internal/constants" 6 | "github.com/hyperjumptech/hansip/pkg/helper" 7 | log "github.com/sirupsen/logrus" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | var ( 13 | trxMiddlewareLog = log.WithField("go", "TrackingMiddleware") 14 | ) 15 | 16 | // TransactionIDMiddleware handles X-Request-Id handler, if no X-Request-Id found, it will create one. 17 | func TransactionIDMiddleware(next http.Handler) http.Handler { 18 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | requestID := r.Header.Get(constants.RequestIDHeader) 20 | if len(requestID) == 0 { 21 | requestID = helper.MakeRandomString(20, true, true, true, false) 22 | } 23 | log := trxMiddlewareLog.WithField("path", r.URL.Path).WithField("RequestID", requestID).WithField("func", "TransactionIDMiddleware").WithField("method", r.Method) 24 | log.Tracef("request start") 25 | start := time.Now() 26 | ctx := context.WithValue(r.Context(), constants.RequestID, requestID) 27 | next.ServeHTTP(w, r.WithContext(ctx)) 28 | dur := time.Now().Sub(start) 29 | log.WithField("ms", dur.Milliseconds()).Tracef("request end") 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 28 | 29 | -------------------------------------------------------------------------------- /api/StaticMiddleware.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hyperjumptech/hansip/internal/config" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | apiPrefix = config.Get("api.path.prefix") 12 | 13 | StaticResources map[string][]byte 14 | MimeTypes map[string]string 15 | ) 16 | 17 | func init() { 18 | resMap, mimMap := GetStaticResource() 19 | StaticResources = resMap 20 | MimeTypes = mimMap 21 | } 22 | 23 | func ServeStatic(w http.ResponseWriter, r *http.Request) { 24 | if r.URL.Path == "/docs" || r.URL.Path == "/docs/" { 25 | http.Redirect(w, r, "/docs/index.html", 301) 26 | } else { 27 | if binary, ok := StaticResources[r.URL.Path]; ok { 28 | if r.URL.Path == "/docs/spec/hansip-api.json" { 29 | data := strings.ReplaceAll(string(binary), `"basePath": "/api/v1/",`, fmt.Sprintf(`"basePath": "%s",`, apiPrefix)) 30 | w.Header().Add("Content-Type", MimeTypes[r.URL.Path]) 31 | w.WriteHeader(http.StatusOK) 32 | _, _ = w.Write([]byte(data)) 33 | } else if r.URL.Path == "/docs/spec/hansip-api.yml" { 34 | data := strings.ReplaceAll(string(binary), `basePath: "/api/v1/"`, fmt.Sprintf(`basePath: "%s"`, apiPrefix)) 35 | w.Header().Add("Content-Type", MimeTypes[r.URL.Path]) 36 | w.WriteHeader(http.StatusOK) 37 | _, _ = w.Write([]byte(data)) 38 | } else { 39 | w.Header().Add("Content-Type", MimeTypes[r.URL.Path]) 40 | w.WriteHeader(http.StatusOK) 41 | _, _ = w.Write(binary) 42 | } 43 | } else { 44 | w.WriteHeader(http.StatusNotFound) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Fork Process 9 | 10 | 1. Ensure that you've installed the Golang (minimum 1.13) in your system. 11 | 2. For this project into your own Github account. 12 | 3. Clone the `hansip` forked repository on your account. 13 | 4. Enter the cloned directory. 14 | 5. Apply new "upstream" to original `hyperjumptech/hansip` git 15 | 6. Now you can work on your account 16 | 7. Remember to pull from your upstream often. `git pull upstream master` 17 | 18 | ## Pull Request Process 19 | 20 | 1. Make sure you always have the most recent update from your upstream. `git pull upstream master` 21 | 2. Resolve all conflict, if any. 22 | 3. Make sure `make test` always successful (you wont be able to create pull request if this fail, circle-ci, travis-ci and azure-devops will make sure of this.) 23 | 4. Push your code to your project's master repository. 24 | 5. Create PullRequest. 25 | * Go to `hithub.com/hyperjumptech/hansip` 26 | * Select `Pull Request` tab 27 | * Click "New pull request" button 28 | * Click "compare across fork" 29 | * Change the source head repository from your fork and target is `hyperjumptech/hansip` 30 | * Hit the "Create pull request" button 31 | * Fill in all necessary information to help us understand about your pull request. 32 | 33 | -------------------------------------------------------------------------------- /internal/hansiperrors/HansipErrors.go: -------------------------------------------------------------------------------- 1 | package hansiperrors 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type ErrPathNotAllowed struct { 9 | Given string 10 | Required string 11 | } 12 | 13 | func (e *ErrPathNotAllowed) Error() string { 14 | return fmt.Sprintf("path permission error. path %s not allowed for %s", e.Given, e.Required) 15 | } 16 | 17 | type ErrMethodNotAllowed struct { 18 | } 19 | 20 | func (e *ErrMethodNotAllowed) Error() string { 21 | return "method is not allowed error" 22 | } 23 | 24 | type ErrAudienceNotAllowed struct { 25 | Audiences []string 26 | } 27 | 28 | func (e *ErrAudienceNotAllowed) Error() string { 29 | return fmt.Sprintf("audiences are not allowed error. %s", strings.Join(e.Audiences, ", ")) 30 | } 31 | 32 | type ErrMissingAuthorizationHeader struct { 33 | } 34 | 35 | func (e *ErrMissingAuthorizationHeader) Error() string { 36 | return "no Authorization header error" 37 | } 38 | 39 | type ErrInvalidAuthorizationMethod struct { 40 | } 41 | 42 | func (e *ErrInvalidAuthorizationMethod) Error() string { 43 | return "authorization error. Authorization header contains non bearer method" 44 | } 45 | 46 | type ErrTokenInvalid struct { 47 | Wrapped error 48 | } 49 | 50 | func (e *ErrTokenInvalid) Error() string { 51 | return fmt.Sprintf("token validation error. Got %s", e.Wrapped.Error()) 52 | } 53 | 54 | func (e *ErrTokenInvalid) Unwrap() error { 55 | return e.Wrapped 56 | } 57 | 58 | type ErrInvalidIssuer struct { 59 | InvalidIssuer string 60 | } 61 | 62 | func (e *ErrInvalidIssuer) Error() string { 63 | return fmt.Sprintf("invalid issuer \"%s\" error", e.InvalidIssuer) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/totp/Secret.go: -------------------------------------------------------------------------------- 1 | package totp 2 | 3 | import ( 4 | "encoding/base32" 5 | "fmt" 6 | "math/rand" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // Secret is a secret data that represent user's secret. 12 | type Secret []byte 13 | 14 | // SecretFromBase32 will create a secret data from its base32 format. 15 | func SecretFromBase32(base32string string) Secret { 16 | dec, err := base32.StdEncoding.DecodeString(base32string) 17 | if err != nil { 18 | return Secret{1} 19 | } 20 | return dec 21 | } 22 | 23 | // MakeSecret will create a random 10 bytes (80 bits) secret. 24 | func MakeSecret() Secret { 25 | rand.Seed(time.Now().Unix()) 26 | secret := make(Secret, 10) 27 | for i := 0; i < 10; i++ { 28 | secret[i] = byte(rand.Intn(255)) 29 | } 30 | return secret 31 | } 32 | 33 | // Base32 will create the Base32 string representation of this secret (eg. for storing into db) 34 | func (s Secret) Base32() string { 35 | return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(s) 36 | } 37 | 38 | // IsValid validate this key. if its 10 bytes than its valid. 39 | func (s Secret) IsValid() bool { 40 | return len(s) == 10 41 | } 42 | 43 | // ProvisionURL will create provisioning URL to be generated into QR or Bar Codes. 44 | // https://github.com/google/google-authenticator/wiki/Key-Uri-Format 45 | func (s Secret) ProvisionURL(issuer, user string) string { 46 | query := make(url.Values) 47 | query.Add("secret", s.Base32()) 48 | query.Add("issuer", issuer) 49 | query.Add("algorithm", "SHA1") 50 | query.Add("digits", "6") 51 | query.Add("period", "30") 52 | return fmt.Sprintf("otpauth://totp/%s:%s?%s", issuer, user, query.Encode()) 53 | } 54 | -------------------------------------------------------------------------------- /api/swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /internal/endpoint/Endpoint_test.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import "testing" 4 | 5 | type TestAccess struct { 6 | Path string 7 | Method uint8 8 | Roles []string 9 | ExpectNoError bool 10 | } 11 | 12 | func TestEndpoint_CanAccess(t *testing.T) { 13 | e := Endpoint{ 14 | PathPattern: "/v1/test/{someparam}/{otherparam}", 15 | IsPublic: false, 16 | AllowedMethodFlag: OptionMethod | GetMethod, 17 | WhiteListAudiences: []string{"admin@*", "user@*"}, 18 | HandleFunction: nil, 19 | } 20 | 21 | testData := []*TestAccess{ 22 | // test roles 23 | &TestAccess{"/v1/test/abc/def", OptionMethod, []string{"admin@tokopedia"}, true}, 24 | &TestAccess{"/v1/test/def/abc", OptionMethod, []string{"admin@tokopedia"}, true}, 25 | &TestAccess{"/v1/test/abc/def", OptionMethod, []string{}, false}, 26 | &TestAccess{"/v1/test/abc/def", OptionMethod, []string{"user@tokopedia"}, true}, 27 | &TestAccess{"/v1/test/abc/def", OptionMethod, []string{"anonymous@tokopedia"}, false}, 28 | &TestAccess{"/v1/test/abc/def", OptionMethod, []string{"anonymous@tokopedia", "admin@tokopedia"}, true}, 29 | &TestAccess{"/v1/test/abc/def", OptionMethod, []string{"anonymous@tokopedia", "unknown@tokopedia"}, false}, 30 | // test paths 31 | &TestAccess{"/v1/test/what/abc/def", OptionMethod, []string{"admin@tokopedia"}, false}, 32 | &TestAccess{"/v1/what/abc/def", OptionMethod, []string{"admin@tokopedia"}, false}, 33 | // test method 34 | &TestAccess{"/v1/test/abc/def", PutMethod, []string{"admin@tokopedia"}, false}, 35 | } 36 | 37 | for i, tst := range testData { 38 | if tst.ExpectNoError && e.canAccess(tst.Path, tst.Method, tst.Roles) != nil { 39 | t.Logf("#%d fail", i) 40 | t.Fail() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/helper/HealthCheck.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const ( 10 | // StatusPass if the health check is ok 11 | StatusPass = "pass" 12 | // StatusFail if the health check is failing 13 | StatusFail = "fail" 14 | // StatusWarn if the health check has some component warning 15 | StatusWarn = "warn" 16 | ) 17 | 18 | // HealthDetail structure shows health check in the component level 19 | type HealthDetail struct { 20 | DetailKey string `json:"-"` 21 | ComponentID string `json:"componentId"` 22 | ComponentType string `json:"componentType"` 23 | MetricValue int `json:"metricValue"` 24 | MetricUnit string `json:"metricUnit"` 25 | Time time.Time `json:"time"` 26 | Status string `json:"status"` 27 | } 28 | 29 | // HealthCheck structure to return when health check is called 30 | type HealthCheck struct { 31 | fmt.Stringer 32 | Status string `json:"status"` 33 | Time time.Time `json:"time"` 34 | Version string `json:"version"` 35 | Details map[string]*HealthDetail `json:"details"` 36 | } 37 | 38 | // AddDetail add health check details 39 | func (hc *HealthCheck) AddDetail(detail *HealthDetail) { 40 | if hc.Details == nil { 41 | hc.Details = make(map[string]*HealthDetail) 42 | } 43 | hc.Details[detail.DetailKey] = detail 44 | } 45 | 46 | // String returns Health check json 47 | func (hc *HealthCheck) String() string { 48 | hc.Time = time.Now() 49 | hc.Version = "1" 50 | hc.Status = StatusPass 51 | if hc.Details == nil || len(hc.Details) == 0 { 52 | hc.Status = StatusPass 53 | } else { 54 | for _, v := range hc.Details { 55 | if v.Status != StatusPass { 56 | hc.Status = StatusWarn 57 | } 58 | } 59 | } 60 | bytes, _ := json.Marshal(hc) 61 | return string(bytes) 62 | } 63 | -------------------------------------------------------------------------------- /internal/endpoint/JwtMiddleware.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/hyperjumptech/hansip/internal/constants" 10 | "github.com/hyperjumptech/hansip/internal/hansipcontext" 11 | "github.com/hyperjumptech/hansip/internal/hansiperrors" 12 | "github.com/hyperjumptech/hansip/pkg/helper" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var ( 17 | middlewareLog = log.WithField("go", "JwtMiddleware") 18 | ) 19 | 20 | // JwtMiddleware handle authorization check for accessed endpoint by inspecting the Authorization header and look for JWT token. 21 | func JwtMiddleware(next http.Handler) http.Handler { 22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 | for _, ep := range Endpoints { 24 | tok, err := ep.AccessValid(r, TokenFactory) 25 | if err == nil { 26 | middlewareLog.Tracef("Traced Path match %s to %s", r.URL.Path, ep.PathPattern) 27 | hansipContext := &hansipcontext.AuthenticationContext{ 28 | Token: tok.Token, 29 | Subject: tok.Subject, 30 | Audience: tok.Audiences, 31 | TokenType: tok.Additional["type"].(string), 32 | } 33 | tokenCtx := context.WithValue(r.Context(), constants.HansipAuthentication, hansipContext) 34 | next.ServeHTTP(w, r.WithContext(tokenCtx)) 35 | return 36 | } 37 | pathNotAllowedError := &hansiperrors.ErrPathNotAllowed{} 38 | audienceNotAllowedErr := &hansiperrors.ErrAudienceNotAllowed{} 39 | if errors.As(err, &pathNotAllowedError) { 40 | middlewareLog.Tracef("Traced Path Not Allowed %v", err) 41 | } 42 | if errors.As(err, &audienceNotAllowedErr) { 43 | middlewareLog.Tracef("Traced Audience Not Allowed %v", err) 44 | } 45 | 46 | } 47 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, fmt.Sprintf("You are not authorized to access this end point %s", r.URL.Path), nil, nil) 48 | return 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/totp/Otp.go: -------------------------------------------------------------------------------- 1 | package totp 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/binary" 7 | "fmt" 8 | "github.com/skip2/go-qrcode" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | var ( 14 | // ErrInvalidOTP is an error to be returned if the supplied OTP code is not valid. 15 | ErrInvalidOTP = fmt.Errorf("invalid otp code format") 16 | // Window OTP validity stepping window. Set this between 2 to 6. Above that is not secure 17 | Window = 3 18 | ) 19 | 20 | // Authenticate will validate the supplied OTP toward user's secrets. 21 | func Authenticate(secret Secret, suppliedOTP string, inUTC bool) (bool, error) { 22 | if len(suppliedOTP) == 6 && suppliedOTP[0] >= '0' && suppliedOTP[0] <= '9' { 23 | code, err := strconv.Atoi(suppliedOTP) 24 | if err != nil { 25 | return false, ErrInvalidOTP 26 | } 27 | var t0 int 28 | if inUTC { 29 | t0 = int(time.Now().UTC().Unix() / 30) 30 | } else { 31 | t0 = int(time.Now().Unix() / 30) 32 | } 33 | 34 | minT := t0 - (Window / 2) 35 | maxT := t0 + (Window / 2) 36 | for t := minT; t <= maxT; t++ { 37 | if getCurrentCode(secret, int64(t)) == code { 38 | return true, nil 39 | } 40 | } 41 | return false, nil 42 | } 43 | return false, ErrInvalidOTP 44 | } 45 | 46 | func getCurrentCode(secret Secret, value int64) int { 47 | hash := hmac.New(sha1.New, secret) 48 | err := binary.Write(hash, binary.BigEndian, value) 49 | if err != nil { 50 | return -1 51 | } 52 | h := hash.Sum(nil) 53 | offset := h[19] & 0x0f 54 | truncated := binary.BigEndian.Uint32(h[offset : offset+4]) 55 | 56 | truncated &= 0x7fffffff 57 | code := truncated % 1000000 58 | 59 | return int(code) 60 | } 61 | 62 | // MakeTotpQrImage will produce a PNG image bytes to be scanned by OTP apps. 63 | func MakeTotpQrImage(secret Secret, issuer, user string) ([]byte, error) { 64 | url := secret.ProvisionURL(issuer, user) 65 | return qrcode.Encode(url, qrcode.Medium, 256) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/helper/StringHelper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "runtime" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // StringToIntHash will create an int hash out of a string. 12 | // 13 | // captureBit is number of bit to be captured. 14 | // in 64 bit system, the valid value are between 8 to 64 15 | // in 32 bit system, the valid value are between 8 to 32 16 | // note : The higher the captureBit, it'll be more unlikely for two string get the same hash. 17 | // The lower the captureBit, it'll be more likely for two string to have the same hash. 18 | // to capture 11 digit integer, 36 bit to be captured. 19 | func StringToIntHash(txt string, captureBit int) int { 20 | bytes := md5.Sum([]byte(strings.Repeat(txt, 10))) 21 | rets := 0 22 | slots := 0 23 | if strings.Contains(runtime.GOARCH, "64") { 24 | slots = 8 25 | } else { 26 | slots = 4 27 | } 28 | slot := make([]byte, slots) 29 | for i, b := range bytes { 30 | if i < len(slot) { 31 | slot[i] = b 32 | } else { 33 | slot[i%4] = slot[i%4] ^ b 34 | } 35 | } 36 | bits := captureBit 37 | if captureBit > (slots * 8) { 38 | bits = slots * 8 39 | } 40 | if captureBit < 8 { 41 | bits = 8 42 | } 43 | bitmap := fmt.Sprintf("%s%s", strings.Repeat("0", (slots*8)-bits), strings.Repeat("1", bits)) 44 | for i := 0; i < slots; i++ { 45 | inte, err := strconv.ParseInt(bitmap[i*8:(i*8)+8], 2, 64) 46 | if err != nil { 47 | panic(err.Error()) 48 | } 49 | slot[i] = slot[i] & byte(inte) 50 | } 51 | for i := 0; i < slots; i++ { 52 | if i == 0 { 53 | rets = int(slot[i]) 54 | } else { 55 | rets = rets << 8 56 | rets = rets | int(slot[i]) 57 | } 58 | } 59 | return rets 60 | } 61 | 62 | // StringArrayContainString returns true if the array contains specified string. 63 | func StringArrayContainString(array []string, s string) bool { 64 | for _, v := range array { 65 | if v == s { 66 | return true 67 | } 68 | } 69 | return false 70 | } 71 | -------------------------------------------------------------------------------- /internal/connector/DbErrors.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import "fmt" 4 | 5 | type ErrDBCreateTableDuplicate struct { 6 | Wrapped error 7 | Message string 8 | } 9 | 10 | func (err *ErrDBCreateTableDuplicate) Error() string { 11 | return err.Message 12 | } 13 | 14 | func (err *ErrDBCreateTableDuplicate) Unwrap() error { 15 | return err.Wrapped 16 | } 17 | 18 | type ErrDBQueryError struct { 19 | Wrapped error 20 | Message string 21 | SQL string 22 | } 23 | 24 | func (err *ErrDBQueryError) Error() string { 25 | return err.Message 26 | } 27 | 28 | func (err *ErrDBQueryError) Unwrap() error { 29 | return err.Wrapped 30 | } 31 | 32 | type ErrDBExecuteError struct { 33 | Wrapped error 34 | Message string 35 | SQL string 36 | } 37 | 38 | func (err *ErrDBExecuteError) Error() string { 39 | return err.Message 40 | } 41 | 42 | func (err *ErrDBExecuteError) Unwrap() error { 43 | return err.Wrapped 44 | } 45 | 46 | type ErrDBScanError struct { 47 | Wrapped error 48 | Message string 49 | SQL string 50 | } 51 | 52 | func (err *ErrDBScanError) Error() string { 53 | return err.Message 54 | } 55 | 56 | func (err *ErrDBScanError) Unwrap() error { 57 | return err.Wrapped 58 | } 59 | 60 | type ErrLibraryCallError struct { 61 | Wrapped error 62 | Message string 63 | LibraryName string 64 | } 65 | 66 | func (err *ErrLibraryCallError) Error() string { 67 | return err.Message 68 | } 69 | 70 | func (err *ErrLibraryCallError) Unwrap() error { 71 | return err.Wrapped 72 | } 73 | 74 | type ErrGroupAndRoleDomainIncompatible struct { 75 | RoleName string 76 | RoleDomain string 77 | GroupName string 78 | GroupDomain string 79 | } 80 | 81 | func (err *ErrGroupAndRoleDomainIncompatible) Error() string { 82 | return fmt.Sprintf("Can not create group role with between incompatible domain Group: %s@%s to Role: %s@%s", err.GroupName, err.GroupDomain, err.RoleName, err.RoleDomain) 83 | } 84 | 85 | type ErrDBNoResult struct { 86 | Message string 87 | SQL string 88 | } 89 | 90 | func (err *ErrDBNoResult) Error() string { 91 | return err.Message 92 | } 93 | -------------------------------------------------------------------------------- /pkg/store/cache/ObjectCache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestNewInMemoryCache_Store(t *testing.T) { 10 | cache := NewInMemoryCache(10, 1, false) 11 | cache.Store("1", "ABC") 12 | cache.Store("1", "BCD") 13 | cache.Store("1", "CDE") 14 | cache.Store("1", "DEF") 15 | 16 | ok, str := cache.Fetch("1") 17 | assert.True(t, ok) 18 | assert.Equal(t, "DEF", str.(string)) 19 | 20 | assert.Equal(t, 1, cache.Size()) 21 | 22 | cache.Delete("1") 23 | ok, str = cache.Fetch("1") 24 | assert.False(t, ok) 25 | } 26 | 27 | func TestNewInMemoryCache_Capacity(t *testing.T) { 28 | cache := NewInMemoryCache(5, 1, false) 29 | cache.Store("1", "ABC") 30 | cache.Store("2", "BCD") 31 | cache.Store("3", "CDE") 32 | cache.Store("4", "DEF") 33 | cache.Store("5", "EFG") 34 | cache.Store("6", "FGH") 35 | cache.Store("7", "GHI") 36 | cache.Store("8", "HIJ") 37 | cache.Store("9", "IJK") 38 | cache.Store("10", "JKL") 39 | assert.Equal(t, 10, cache.Size()) 40 | cache.Store("11", "ABC") 41 | assert.Equal(t, 10, cache.Size()) 42 | cache.Store("12", "BCD") 43 | assert.Equal(t, 10, cache.Size()) 44 | cache.Store("13", "CDE") 45 | assert.Equal(t, 10, cache.Size()) 46 | 47 | ok, _ := cache.Fetch("1") 48 | assert.False(t, ok) 49 | ok, _ = cache.Fetch("2") 50 | assert.False(t, ok) 51 | ok, _ = cache.Fetch("3") 52 | assert.False(t, ok) 53 | ok, _ = cache.Fetch("4") 54 | assert.True(t, ok) 55 | ok, _ = cache.Fetch("5") 56 | assert.True(t, ok) 57 | ok, _ = cache.Fetch("6") 58 | assert.True(t, ok) 59 | ok, _ = cache.Fetch("7") 60 | assert.True(t, ok) 61 | ok, _ = cache.Fetch("8") 62 | assert.True(t, ok) 63 | 64 | time.Sleep(1001 * time.Millisecond) 65 | 66 | assert.Equal(t, 0, cache.Size()) 67 | } 68 | 69 | func TestInMemoryCache_Fetch(t *testing.T) { 70 | cache := NewInMemoryCache(10, 1, false) 71 | cache.Store("1", "ABC") 72 | cache.Store("2", "BCA") 73 | time.Sleep(1001 * time.Millisecond) 74 | ok, _ := cache.Fetch("1") 75 | assert.False(t, ok) 76 | ok, _ = cache.Fetch("2") 77 | assert.False(t, ok) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/helper/TokenFactory_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var ( 9 | signKey = "thisisatestsignkey" 10 | signMethod = "HS256" 11 | issuer = "anIssuer" 12 | subject = "aSubject" 13 | audience = []string{"aud1", "aud2"} 14 | issuedAt = time.Date(2010, 1, 1, 1, 0, 0, 0, time.UTC) 15 | notBefore = time.Date(2019, 1, 1, 1, 0, 0, 0, time.UTC) 16 | expiry = time.Date(2030, 1, 1, 1, 0, 0, 0, time.UTC) 17 | additional = map[string]interface{}{ 18 | "type": "access", 19 | } 20 | token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYXVkMSIsImF1ZDIiXSwiZXhwIjoxODkzNDU5NjAwLCJpYXQiOjEyNjIzMDc2MDAsImlzcyI6ImFuSXNzdWVyIiwibmJmIjoxNTQ2MzA0NDAwLCJzdWIiOiJhU3ViamVjdCIsInR5cGUiOiJhY2Nlc3MifQ.VsqnUp2kapOFUHhNvP75RNDgicc7iN_SZF34LkAAWoo" 21 | ) 22 | 23 | func TestCreateJWTStringToken(t *testing.T) { 24 | tok, err := CreateJWTStringToken(signKey, signMethod, issuer, subject, audience, issuedAt, notBefore, expiry, additional) 25 | if err != nil { 26 | t.Errorf("got %s", err) 27 | t.Fail() 28 | } 29 | if tok != token { 30 | t.Errorf("token not match\n%s\ngot\n%s", token, tok) 31 | t.Fail() 32 | } 33 | } 34 | 35 | func TestReadJWTStringToken(t *testing.T) { 36 | iss, sub, aud, iat, nbf, exp, add, err := ReadJWTStringToken(true, signKey, signMethod, token) 37 | if err != nil { 38 | t.Errorf("got %s", err) 39 | t.Fail() 40 | } 41 | if iss != issuer { 42 | t.Errorf("expect issuer %s but %s", issuer, iss) 43 | } 44 | if sub != subject { 45 | t.Errorf("expect subject %s but %s", subject, sub) 46 | } 47 | if aud[0] != audience[0] { 48 | t.Errorf("expect audience[0] %s but %s", audience[0], aud[0]) 49 | } 50 | if aud[1] != audience[1] { 51 | t.Errorf("expect audience[1] %s but %s", audience[1], aud[1]) 52 | } 53 | if !nbf.Equal(notBefore) { 54 | t.Errorf("expect notBefore %s but %s", notBefore, nbf) 55 | } 56 | if !iat.Equal(issuedAt) { 57 | t.Errorf("expect issuedAt %s but %s", issuedAt, iat) 58 | } 59 | if !exp.Equal(expiry) { 60 | t.Errorf("expect expiry %s but %s", expiry, exp) 61 | } 62 | if additional["type"] != add["type"] { 63 | t.Errorf("expect type %s but %s", additional["type"], add["type"]) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/connector/hansip.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS HANSIP_USER_GROUP, HANSIP_USER_ROLE, HANSIP_GROUP_ROLE, HANSIP_USER, HANSIP_GROUP, HANSIP_ROLE; 2 | 3 | CREATE TABLE IF NOT EXISTS HANSIP_USER ( 4 | REC_ID VARCHAR(32) NOT NULL UNIQUE, 5 | EMAIL VARCHAR(128) NOT NULL UNIQUE, 6 | HASHED_PASSPHRASE VARCHAR(128), 7 | ENABLED TINYINT(1) UNSIGNED DEFAULT 0, 8 | SUSPENDED TINYINT(1) UNSIGNED DEFAULT 0, 9 | LAST_SEEN DATETIME, 10 | LAST_LOGIN DATETIME, 11 | FAIL_COUNT INT DEFAULT 0, 12 | ACTIVATION_CODE VARCHAR(32), 13 | ACTIVATION_DATE DATETIME, 14 | TOTP_KEY VARCHAR(64), 15 | ENABLE_2FE TINYINT(1) UNSIGNED DEFAULT 0, 16 | TOKEN_2FE VARCHAR(10), 17 | RECOVERY_CODE VARCHAR (20), 18 | PRIMARY KEY (REC_ID) 19 | ) ENGINE=INNODB; 20 | 21 | CREATE TABLE IF NOT EXISTS HANSIP_GROUP ( 22 | REC_ID VARCHAR(32) NOT NULL UNIQUE, 23 | GROUP_NAME VARCHAR(128) NOT NULL UNIQUE, 24 | DESCRIPTION VARCHAR(255), 25 | PRIMARY KEY (REC_ID) 26 | ) ENGINE=INNODB; 27 | 28 | CREATE TABLE IF NOT EXISTS HANSIP_ROLE ( 29 | REC_ID VARCHAR(32) NOT NULL UNIQUE, 30 | ROLE_NAME VARCHAR(128) NOT NULL UNIQUE, 31 | DESCRIPTION VARCHAR(255), 32 | PRIMARY KEY (REC_ID) 33 | ) ENGINE=INNODB; 34 | 35 | CREATE TABLE IF NOT EXISTS HANSIP_USER_ROLE ( 36 | USER_REC_ID VARCHAR(32) NOT NULL, 37 | ROLE_REC_ID VARCHAR(32) NOT NULL, 38 | PRIMARY KEY (USER_REC_ID,ROLE_REC_ID), 39 | FOREIGN KEY (USER_REC_ID) REFERENCES HANSIP_USER(REC_ID) ON DELETE CASCADE, 40 | FOREIGN KEY (ROLE_REC_ID) REFERENCES HANSIP_ROLE(REC_ID) ON DELETE CASCADE 41 | ) ENGINE=INNODB; 42 | 43 | CREATE TABLE IF NOT EXISTS HANSIP_USER_GROUP ( 44 | USER_REC_ID VARCHAR(32) NOT NULL, 45 | GROUP_REC_ID VARCHAR(32) NOT NULL, 46 | PRIMARY KEY (USER_REC_ID,GROUP_REC_ID), 47 | FOREIGN KEY (USER_REC_ID) REFERENCES HANSIP_USER(REC_ID) ON DELETE CASCADE, 48 | FOREIGN KEY (GROUP_REC_ID) REFERENCES HANSIP_GROUP(REC_ID) ON DELETE CASCADE 49 | ) ENGINE=INNODB; 50 | 51 | CREATE TABLE IF NOT EXISTS HANSIP_GROUP_ROLE ( 52 | GROUP_REC_ID VARCHAR(32) NOT NULL, 53 | ROLE_REC_ID VARCHAR(32) NOT NULL, 54 | PRIMARY KEY (GROUP_REC_ID,ROLE_REC_ID), 55 | FOREIGN KEY (GROUP_REC_ID) REFERENCES HANSIP_GROUP(REC_ID) ON DELETE CASCADE, 56 | FOREIGN KEY (ROLE_REC_ID) REFERENCES HANSIP_ROLE(REC_ID) ON DELETE CASCADE 57 | ) ENGINE=INNODB; 58 | -------------------------------------------------------------------------------- /api/swagger-ui/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 68 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPATH=$(shell go env GOPATH) 2 | IMAGE_REGISTRY=dockerhub 3 | IMAGE_NAMESPACE ?= hansip 4 | IMAGE_NAME ?= $(shell basename `pwd`) 5 | CURRENT_PATH=$(shell pwd) 6 | COMMIT_ID ?= $(shell git rev-parse --short HEAD) 7 | GO111MODULE=on 8 | 9 | .PHONY: all test clean build docker 10 | 11 | build-static: 12 | -${GOPATH}/bin/go-resource -base "$(CURRENT_PATH)/api/swagger-ui" -path "/docs" -filter "/**/*" -go "$(CURRENT_PATH)/api/StaticApi.go" -package api 13 | go fmt ./... 14 | 15 | build: build-static 16 | # export GO111MODULE=on; \ 17 | # GO_ENABLED=0 go build -a -o $(IMAGE_NAME).app cmd/main/Main.go 18 | # Use bellow if you're running on linux. 19 | GO_ENABLED=0 go build -a -o $(IMAGE_NAME).app cmd/main/Main.go 20 | 21 | lint: build-static 22 | # golint -set_exit_status ./internal/... ./pkg/... ./cmd/... 23 | 24 | test: lint 25 | # go install github.com/newm4n/goornogo 26 | export GO111MODULE on; \ 27 | go test ./... -cover -vet -all -v -short -covermode=count -coverprofile=coverage.out 28 | # goornogo -i coverage.out -c 30 29 | 30 | run: build 31 | export AAA_SERVER_HOST=0.0.0.0; \ 32 | export AAA_SERVER_PORT=8088; \ 33 | export AAA_SETUP_ADMIN_ENABLE=true; \ 34 | export AAA_SERVER_LOG_LEVEL=TRACE; \ 35 | export AAA_SERVER_HTTP_CORS_ENABLE=true; \ 36 | export AAA_SERVER_HTTP_CORS_ALLOW_ORIGINS=*; \ 37 | export AAA_SERVER_HTTP_CORS_ALLOW_CREDENTIAL=true; \ 38 | export AAA_SERVER_HTTP_CORS_ALLOW_METHOD=GET,PUT,DELETE,POST,OPTIONS; \ 39 | export AAA_SERVER_HTTP_CORS_ALLOW_HEADERS=Accept,Authorization,Content-Type,X-CSRF-TOKEN,Accept-Encoding; \ 40 | export AAA_SERVER_HTTP_CORS_EXPOSED_HEADERS=*; \ 41 | export AAA_SERVER_HTTP_CORS_IGNOREOPTION=false; \ 42 | export AAA_SERVER_HTTP_CORS_OPTIONSTATUS=200; \ 43 | export AAA_TOKEN_ISSUER=aaa.hansip.go.id; \ 44 | export AAA_MAILER_TYPE=DUMMY; \ 45 | export AAA_MAILER_FROM=aaa@hansip.go.id; \ 46 | ./$(IMAGE_NAME).app 47 | rm -f $(IMAGE_NAME).app 48 | 49 | docker: 50 | docker build -t $(IMAGE_NAMESPACE)/$(IMAGE_NAME):latest -f ./.docker/Dockerfile . 51 | 52 | docker-build-commit: build 53 | docker build -t $(IMAGE_NAMESPACE)/$(IMAGE_NAME):$(COMMIT_ID) -f ./.docker/Dockerfile . 54 | 55 | docker-build: build 56 | docker build -t $(IMAGE_NAMESPACE)/$(IMAGE_NAME):$(COMMIT_ID) -f ./.docker/Dockerfile . 57 | docker tag $(IMAGE_NAMESPACE)/$(IMAGE_NAME):$(COMMIT_ID) $(IMAGE_NAMESPACE)/$(IMAGE_NAME):latest 58 | 59 | docker-push: 60 | docker push $(IMAGE_NAMESPACE)/$(IMAGE_NAME):$(COMMIT_ID) 61 | 62 | docker-stop: 63 | -docker stop $(IMAGE_NAME) 64 | 65 | docker-rm: docker-stop 66 | -docker rm $(IMAGE_NAME) 67 | 68 | docker-run: docker-rm docker 69 | docker run --name $(IMAGE_NAME) -p 3000:3000 --detach $(IMAGE_NAMESPACE)/$(IMAGE_NAME):latest 70 | -------------------------------------------------------------------------------- /pkg/helper/HttpHelper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/hyperjumptech/hansip/internal/constants" 8 | log "github.com/sirupsen/logrus" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | // ResponseJSON define the structure of all response 14 | type ResponseJSON struct { 15 | HTTPCode int `json:"httpcode"` 16 | Message string `json:"message"` 17 | Status string `json:"status"` 18 | Data interface{} `json:"data,omitempty"` 19 | } 20 | 21 | // ParsePathParams parse request path param according to path template and extract its values. 22 | func ParsePathParams(template, path string) (map[string]string, error) { 23 | var pth string 24 | if strings.Contains(path, "?") { 25 | pth = path[:strings.Index(path, "?")] 26 | } else { 27 | pth = path 28 | } 29 | templatePaths := strings.Split(template, "/") 30 | pathPaths := strings.Split(pth, "/") 31 | if len(templatePaths) != len(pathPaths) { 32 | return nil, fmt.Errorf("pathElement length not equals to templateElement length") 33 | } 34 | ret := make(map[string]string) 35 | for idx, templateElement := range templatePaths { 36 | pathElement := pathPaths[idx] 37 | if len(templateElement) > 0 && len(pathElement) > 0 { 38 | if templateElement[:1] == "{" && templateElement[len(templateElement)-1:] == "}" { 39 | tKey := templateElement[1 : len(templateElement)-1] 40 | ret[tKey] = pathElement 41 | } else if templateElement != pathElement { 42 | return nil, fmt.Errorf("template %s not compatible with path %s", template, path) 43 | } 44 | } 45 | } 46 | return ret, nil 47 | } 48 | 49 | // WriteHTTPResponse into the response writer, according to the response code and headers. 50 | // headerMap and data argument are both optional 51 | func WriteHTTPResponse(ctx context.Context, w http.ResponseWriter, httpRespCode int, message string, headerMap map[string]string, data interface{}) { 52 | w.Header().Add("Content-Type", "application/json") 53 | if headerMap != nil { 54 | for k, v := range headerMap { 55 | w.Header().Add(k, v) 56 | } 57 | } 58 | if ctx.Value(constants.RequestID) != nil { 59 | w.Header().Add("X-Request-ID", ctx.Value(constants.RequestID).(string)) 60 | } 61 | w.WriteHeader(httpRespCode) 62 | rJSON := &ResponseJSON{ 63 | HTTPCode: httpRespCode, 64 | Message: message, 65 | Data: data, 66 | Status: "FAIL", 67 | } 68 | if httpRespCode >= 200 && httpRespCode < 400 { 69 | rJSON.Status = "SUCCESS" 70 | if len(rJSON.Message) == 0 { 71 | rJSON.Message = "Operation Success" 72 | } 73 | } else { 74 | rJSON.Status = "FAIL" 75 | if len(rJSON.Message) == 0 { 76 | rJSON.Message = "Operation Failed" 77 | } 78 | } 79 | bytes, err := json.Marshal(rJSON) 80 | if err != nil { 81 | log.Errorf("Can not marshal. Got %s", err) 82 | } else { 83 | i, err := w.Write(bytes) 84 | if err != nil { 85 | log.Errorf("Can not write byte stream. Got %s. %d bytes written", err, i) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCTS.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ### Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ### Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ### Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ### Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `oss@hyperjump.tech`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ### Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /internal/gzip/GzipEncodingMiddleware.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | gzipFilterLog = logrus.WithFields(logrus.Fields{ 14 | "module": "GZIP Filter", 15 | "gofile": "GzipEncodingFilter.go", 16 | }) 17 | ) 18 | 19 | // NewGzipEncoderFilter creates new encoder filter that handles gzip compression. 20 | func NewGzipEncoderFilter(enable bool, minSizeToCompress int) *EncoderFilter { 21 | if !enable { 22 | gzipFilterLog.Warnf("GZIP Compression response body is DISABLED. Should be enabled for best performance.") 23 | } 24 | return &EncoderFilter{ 25 | EnableGzip: enable, 26 | GzipMinSize: minSizeToCompress, 27 | } 28 | } 29 | 30 | // EncoderFilter is struct to host Middleware function DoFilter and store minimum data size for gzip 31 | type EncoderFilter struct { 32 | EnableGzip bool 33 | GzipMinSize int 34 | } 35 | 36 | // DoFilter will return the middleware function for compressing body IF the client ask for Accept-Encoding: gzip 37 | func (filter *EncoderFilter) DoFilter(next http.Handler) http.Handler { 38 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | // Check if the client can accept the gzip encoding. 40 | if filter.EnableGzip == false || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { 41 | // The client cannot accept it, so return the output 42 | // uncompressed. 43 | gzipFilterLog.Tracef("Enable gzip is %v. Accept-Encoding is %s ", filter.EnableGzip, r.Header.Get("Accept-Encoding")) 44 | next.ServeHTTP(w, r) 45 | return 46 | } 47 | 48 | // serve the request using new recorder. 49 | recorder := httptest.NewRecorder() 50 | next.ServeHTTP(recorder, r) 51 | bodyBytes := recorder.Body.Bytes() 52 | 53 | containsContentType := false 54 | 55 | // write the rest of the headers. 56 | for key, v := range recorder.Header() { 57 | for _, vv := range v { 58 | if strings.ToLower(key) == "content-type" { 59 | containsContentType = true 60 | gzipFilterLog.Tracef("%s: %s already exist.", key, vv) 61 | } 62 | w.Header().Set(key, vv) 63 | } 64 | } 65 | 66 | // If its non 2xx we dont compress it. 67 | if recorder.Code < 200 || recorder.Code >= 300 { 68 | // write the result. 69 | w.WriteHeader(recorder.Code) 70 | // write the body after write header so golang http will not temper to the response code 71 | w.Write(bodyBytes) 72 | return 73 | } 74 | 75 | if !containsContentType { 76 | ctype := http.DetectContentType(bodyBytes) 77 | gzipFilterLog.Tracef("Content-Type not exist. Assigning one with Content-Type: %s. ", ctype) 78 | w.Header().Set("Content-Type", ctype) 79 | } 80 | 81 | // if the body size is above minimum size zip them. 82 | if len(bodyBytes) > filter.GzipMinSize { 83 | 84 | // add header for gzip content encoding 85 | w.Header().Set("Content-Encoding", "gzip") 86 | // write the result. 87 | w.WriteHeader(recorder.Code) 88 | 89 | // create empty byte buffer. 90 | buff := bytes.NewBuffer(make([]byte, 0)) 91 | 92 | // Create new gzip writer to write gzip result into empty buffer. 93 | gw := gzip.NewWriter(buff) 94 | 95 | // Write the original content into gzip writer. 96 | written, err := gw.Write(bodyBytes) 97 | if err != nil { 98 | gzipFilterLog.Errorf("Error while writing to gzip writer. got %v", err) 99 | } 100 | gw.Close() 101 | gzipFilterLog.Tracef("Written into gzip writer %d bytes, yielding %d bytes.", written, len(buff.Bytes())) 102 | 103 | // Write the gzip result into response body. 104 | w.Write(buff.Bytes()) 105 | 106 | } else { 107 | // write the result. 108 | w.WriteHeader(recorder.Code) 109 | // if the body size is bellow minimum size to zip, return them as is. 110 | w.Write(bodyBytes) 111 | } 112 | 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/go,intellij 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,intellij 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | *.app 13 | secret.txt 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | ### Go Patch ### 25 | /vendor/ 26 | /Godeps/ 27 | 28 | ### Intellij ### 29 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 30 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 31 | 32 | # User-specific stuff 33 | .idea/**/workspace.xml 34 | .idea/**/tasks.xml 35 | .idea/**/usage.statistics.xml 36 | .idea/**/dictionaries 37 | .idea/**/shelf 38 | 39 | # Generated files 40 | .idea/**/contentModel.xml 41 | 42 | # Sensitive or high-churn files 43 | .idea/**/dataSources/ 44 | .idea/**/dataSources.ids 45 | .idea/**/dataSources.local.xml 46 | .idea/**/sqlDataSources.xml 47 | .idea/**/dynamic.xml 48 | .idea/**/uiDesigner.xml 49 | .idea/**/dbnavigator.xml 50 | 51 | # Gradle 52 | .idea/**/gradle.xml 53 | .idea/**/libraries 54 | 55 | # Gradle and Maven with auto-import 56 | # When using Gradle or Maven with auto-import, you should exclude module files, 57 | # since they will be recreated, and may cause churn. Uncomment if using 58 | # auto-import. 59 | # .idea/artifacts 60 | # .idea/compiler.xml 61 | # .idea/jarRepositories.xml 62 | # .idea/modules.xml 63 | # .idea/*.iml 64 | # .idea/modules 65 | # *.iml 66 | # *.ipr 67 | 68 | # CMake 69 | cmake-build-*/ 70 | 71 | # Mongo Explorer plugin 72 | .idea/**/mongoSettings.xml 73 | 74 | # File-based project format 75 | *.iws 76 | 77 | # IntelliJ 78 | out/ 79 | 80 | # mpeltonen/sbt-idea plugin 81 | .idea_modules/ 82 | 83 | # JIRA plugin 84 | atlassian-ide-plugin.xml 85 | 86 | # Cursive Clojure plugin 87 | .idea/replstate.xml 88 | 89 | # Crashlytics plugin (for Android Studio and IntelliJ) 90 | com_crashlytics_export_strings.xml 91 | crashlytics.properties 92 | crashlytics-build.properties 93 | fabric.properties 94 | 95 | # Editor-based Rest Client 96 | .idea/httpRequests 97 | 98 | # Android studio 3.1+ serialized cache file 99 | .idea/caches/build_file_checksums.ser 100 | 101 | ### Intellij Patch ### 102 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 103 | 104 | # *.iml 105 | # modules.xml 106 | # .idea/misc.xml 107 | # *.ipr 108 | 109 | # Sonarlint plugin 110 | .idea/**/sonarlint/ 111 | 112 | # SonarQube Plugin 113 | .idea/**/sonarIssues.xml 114 | 115 | # Markdown Navigator plugin 116 | .idea/**/markdown-navigator.xml 117 | .idea/**/markdown-navigator-enh.xml 118 | .idea/**/markdown-navigator/ 119 | 120 | # Cache file creation bug 121 | # See https://youtrack.jetbrains.com/issue/JBR-2257 122 | .idea/$CACHE_FILE$ 123 | 124 | ### macOS ### 125 | # General 126 | .DS_Store 127 | .AppleDouble 128 | .LSOverride 129 | 130 | # Icon must end with two \r 131 | Icon 132 | 133 | # Thumbnails 134 | ._* 135 | 136 | # Files that might appear in the root of a volume 137 | .DocumentRevisions-V100 138 | .fseventsd 139 | .Spotlight-V100 140 | .TemporaryItems 141 | .Trashes 142 | .VolumeIcon.icns 143 | .com.apple.timemachine.donotpresent 144 | 145 | # Directories potentially created on remote AFP share 146 | .AppleDB 147 | .AppleDesktop 148 | Network Trash Folder 149 | Temporary Items 150 | .apdisk 151 | 152 | ### VisualStudioCode ### 153 | .vscode/* 154 | !.vscode/settings.json 155 | !.vscode/tasks.json 156 | !.vscode/launch.json 157 | !.vscode/extensions.json 158 | *.code-workspace 159 | 160 | ### VisualStudioCode Patch ### 161 | # Ignore all local history of files 162 | .history 163 | 164 | # End of https://www.toptal.com/developers/gitignore/api/macos,go,intellij 165 | 166 | #ignore executable file from vscode debug 167 | /cmd/main/__debug_bin 168 | #ignore stuffed static files, keeps changing all the time 169 | /api/StaticApi.go -------------------------------------------------------------------------------- /pkg/helper/Pagination.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | ) 7 | 8 | // NewPageRequestFromRequest create new page request information based on the http request query 9 | func NewPageRequestFromRequest(r *http.Request) (*PageRequest, error) { 10 | no := 1 11 | size := 10 12 | queries := r.URL.Query() 13 | order := "" 14 | sorting := "ASC" 15 | 16 | if len(queries.Get("page_no")) > 0 { 17 | pno, err := strconv.Atoi(queries.Get("page_no")) 18 | if err != nil { 19 | return nil, err 20 | } 21 | no = pno 22 | } 23 | 24 | if len(queries.Get("page_size")) > 0 { 25 | psize, err := strconv.Atoi(queries.Get("page_size")) 26 | if err != nil { 27 | return nil, err 28 | } 29 | size = psize 30 | } 31 | 32 | if len(queries.Get("order_by")) > 0 { 33 | order = queries.Get("order_by") 34 | } 35 | 36 | if len(queries.Get("sort")) > 0 { 37 | sorting = queries.Get("sort") 38 | } 39 | 40 | ret := &PageRequest{ 41 | No: uint(no), 42 | PageSize: uint(size), 43 | OrderBy: order, 44 | Sort: sorting, 45 | } 46 | return ret, nil 47 | } 48 | 49 | // NewPage create a new page structure based on page request and total number of items. 50 | func NewPage(pageRequest *PageRequest, totalItems uint) *Page { 51 | page := &Page{ 52 | Sort: pageRequest.Sort, 53 | PageSize: pageRequest.PageSize, 54 | OrderBy: pageRequest.OrderBy, 55 | TotalItems: totalItems, 56 | } 57 | if totalItems == 0 { 58 | page.No = 1 59 | page.TotalPages = 1 60 | page.Items = 0 61 | page.LastPage = 1 62 | page.FistPage = 1 63 | page.NextPage = 1 64 | page.PrevPage = 1 65 | page.OffsetStart = 0 66 | page.OffsetEnd = 0 67 | page.IsFirst = true 68 | page.IsLast = true 69 | return page 70 | } 71 | page.TotalPages = uint(totalItems / pageRequest.PageSize) 72 | if totalItems%pageRequest.PageSize > 0 { 73 | page.TotalPages++ 74 | } 75 | if pageRequest.No < 1 { 76 | page.No = 1 77 | } else if pageRequest.No > page.TotalPages { 78 | page.No = page.TotalPages 79 | } else { 80 | page.No = pageRequest.No 81 | } 82 | if page.No == page.TotalPages { 83 | if totalItems%pageRequest.PageSize > 0 { 84 | page.Items = totalItems % pageRequest.PageSize 85 | } else { 86 | page.Items = pageRequest.PageSize 87 | } 88 | } else { 89 | page.Items = pageRequest.PageSize 90 | } 91 | page.HasNext = page.No < page.TotalPages 92 | page.HasPrev = page.No > 1 93 | page.FistPage = 1 94 | page.LastPage = page.TotalPages 95 | page.NextPage = page.No + 1 96 | if page.NextPage > page.TotalPages { 97 | page.NextPage = page.TotalPages 98 | } 99 | page.PrevPage = page.No - 1 100 | if page.PrevPage < 1 { 101 | page.PrevPage = 1 102 | } 103 | page.OffsetStart = (page.No - 1) * page.PageSize 104 | page.OffsetEnd = page.OffsetStart + page.PageSize 105 | if page.OffsetEnd >= page.TotalItems { 106 | page.OffsetEnd = page.TotalItems 107 | } 108 | if page.No == page.FistPage { 109 | page.IsFirst = true 110 | } 111 | if page.No == page.LastPage { 112 | page.IsLast = true 113 | } 114 | return page 115 | } 116 | 117 | // Page a meta data for listing that contains pagination structure 118 | type Page struct { 119 | No uint `json:"no"` 120 | TotalPages uint `json:"total_pages"` 121 | PageSize uint `json:"page_size"` 122 | Items uint `json:"items"` 123 | TotalItems uint `json:"total_items"` 124 | HasNext bool `json:"has_next"` 125 | HasPrev bool `json:"has_prev"` 126 | IsFirst bool `json:"is_first"` 127 | IsLast bool `json:"is_last"` 128 | FistPage uint `json:"fist_page"` 129 | NextPage uint `json:"next_page"` 130 | PrevPage uint `json:"prev_page"` 131 | LastPage uint `json:"last_page"` 132 | OrderBy string `json:"order_by"` 133 | OffsetStart uint `json:"-"` 134 | OffsetEnd uint `json:"-"` 135 | Sort string `json:"sort"` 136 | } 137 | 138 | // PageRequest define a list query specification in paginated fashion. 139 | type PageRequest struct { 140 | No uint `json:"no"` 141 | PageSize uint `json:"page_size"` 142 | OrderBy string `json:"order_by"` 143 | Sort string `json:"sort"` 144 | } 145 | -------------------------------------------------------------------------------- /pkg/helper/RoleUtil.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // IsRoleValid ini melakukan validasi apakah Role-Role yang menjadi syarat 10 | // untuk mengakses sebuah resource (requires) bisa dipenuhi oleh seorang pengakses 11 | // yang memiliki Role-Role tertentu (supplied). 12 | // 13 | // Sebagai contoh : 14 | // 15 | // Diketahui sebuah path "/artikel/abc" dari URL "https://domain.com/artikel/abc" 16 | // Path ini mewajibkan pengakses harus memiliki role "user@domain.com" dan juga harus memiliki role "reader@domain.com". 17 | // 18 | // Jika pengakses path tersebut, miliki role (bisa di ambil dari database, atau dari token) sebagai berikut. 19 | // "user@domain.com" dan "writer@domain.com" 20 | // 21 | // Maka fungsi IsRoleValid ini bisa dipergunakan apakah pengakses tersebut boleh mengakses path dimaksud. 22 | // 23 | // Allowed := IsRoleValid([]string {"user@domain.com","reader@domain.com"}, []string {"user@domain.com", "writer@domain.com"}) 24 | // 25 | // Allowed adalah nilai boolean, dimana apabila nilainya TRUE maka user boleh mengakses. dan FALSE jika tidak. 26 | // Silahkan lihat testing code dibagian bawah. 27 | // 28 | // Baik requires dan supplied keduanya berisi array string. Dimana requires semuanya WAJIB dipenuhi oleh supplied. 29 | // Jika salah satu role yang disebutkan dalam requires tidak dipenuhi, maka IsRoleValid akan mengembalikan FALSE. 30 | // 31 | // Contoh 1 : 32 | // requires = []string {"user@domain.com", "reader@domain.com"} 33 | // 34 | // Maka IsRoleValid akan mengembalikan nilai TRUE jika : 35 | // 36 | // 1. supplied = []string {"user@domain.com", "reader@domain.com"} // pengakses memiliki role yang diperlukan. 37 | // 2. supplied = []string {"*@domain.com"} // pengakses memiliki role yang secara pola/pattern memenuhi semua syarat role dalam requires. 38 | // 3. supplied = []string {"*@domain.com", "abc@other.com"} // pengakses memiliki role yang salah satunya, secara pola/pattern, memenuhi semua syarat role dalam requires. 39 | // 40 | // Contoh 2 : 41 | // requires = []string {"*@domain.com"} 42 | // 43 | // Maka IsRoleValid akan mengembalikan nilai TRUE jika : 44 | // 45 | // 1. supplied = []string {"user@domain.com"} // pengakses memiliki role yang memenuhi pola syarat role dalam requires. 46 | // 2. supplied = []string {"user@domain.com", "abc@other.com"} // pengakses memiliki role yang salah satunya memenuhi pola syarat role dalam requires. 47 | // 48 | // CATATAN : requires yang memiliki role dengan pola wildcard, tidak bisa dipenuhi dengan supplied yang juga menggunakan pola wildcard. 49 | // Contoh : 50 | // 51 | // NeverAllowed := IsRoleValid([]string {"*@domain.com"}, []string {"writer@*"}); 52 | // 53 | // Silahkan lihat contoh testing di RoleUtil_test.go 54 | func IsRoleValid(requires, supplied []string) bool { 55 | if len(requires) == 0 { 56 | return true 57 | } 58 | valid := false 59 | for _, require := range requires { 60 | requireValid := false 61 | for _, supply := range supplied { 62 | if matchesTwoRole(require, supply) { 63 | valid = true 64 | requireValid = true 65 | } 66 | } 67 | if !requireValid { 68 | return false 69 | } 70 | } 71 | return valid 72 | } 73 | 74 | // matchesTwoRole ini adalah fungsi sederhana yang dipergunakan oleh fungsi IsRoleValid 75 | // untuk membandingkan apakah diantara 2 string bisa saling memenuhi pola role diantara mereka. 76 | // 77 | // Contoh : 78 | // Jika a := "abcd@efghijk.lmn.com" 79 | // 80 | // Maka fungsi MatchesTwoRole akan mengembalikan nilai TRUE jika: 81 | // 1. b := "abcd@efghijk.lmn.com" // dimana b sama persis dengan a. 82 | // 2. b := "*@efghijk.lmn.com" // dimana b memiliki pola yang cocok dengan a. 83 | // 3. b := "*@*" // b memiliki pola yang cocok dengan a (variasi pola) 84 | // 4. b := "*cd@efghijk.*" // b memiliki pola yang cocok dengan a (variasi pola) 85 | func matchesTwoRole(a, b string) bool { 86 | aw := strings.Contains(a, "*") 87 | bw := strings.Contains(b, "*") 88 | if aw && bw { 89 | return false 90 | } 91 | if !aw && !bw { 92 | return a == b 93 | } 94 | if aw { 95 | rx, err := regexp.Compile(fmt.Sprintf("^%s$", strings.ReplaceAll(a, "*", `[a-zA-Z0-9_\-\.]+`))) 96 | if err != nil { 97 | return false 98 | } 99 | return rx.Match([]byte(b)) 100 | } 101 | if bw { 102 | rx, err := regexp.Compile(fmt.Sprintf("^%s$", strings.ReplaceAll(b, "*", `[a-zA-Z0-9_\-\.]+`))) 103 | if err != nil { 104 | return false 105 | } 106 | return rx.Match([]byte(a)) 107 | } 108 | return false 109 | } 110 | -------------------------------------------------------------------------------- /pkg/helper/RoleUtil_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import "testing" 4 | 5 | type RoleCheckTest struct { 6 | Success bool 7 | Required []string 8 | Supplied []string 9 | } 10 | 11 | func TestIsRoleValid(t *testing.T) { 12 | TestData := []RoleCheckTest{ 13 | { 14 | Success: true, 15 | Required: []string{"basic@app.idntimes.com"}, 16 | Supplied: []string{"basic@app.idntimes.com"}, 17 | }, { 18 | Success: false, 19 | Required: []string{"basic@app.idntimes.com"}, 20 | Supplied: []string{"anon@app.idntimes.com"}, 21 | }, { 22 | Success: false, 23 | Required: []string{"basic@app.idntimes.com"}, 24 | Supplied: []string{"anon@app.idntimes.com", "admin@app.idntimes.com"}, 25 | }, { 26 | Success: false, 27 | Required: []string{"reg-sum-edt@app.idntimes.com"}, 28 | Supplied: []string{"reg-sul-edt@app.idntimes.com", "reg-abc-edt@app.idntimes.com"}, 29 | }, { 30 | Success: true, 31 | Required: []string{"reg-sum-edt@app.idntimes.com"}, 32 | Supplied: []string{"reg-sul-edt@app.idntimes.com", "reg-*-edt@app.idntimes.com"}, 33 | }, { 34 | Success: true, 35 | Required: []string{"reg-*-edt@app.idntimes.com"}, 36 | Supplied: []string{"reg-sul-edt@app.idntimes.com", "reg-*-edt@app.idntimes.com"}, 37 | }, { 38 | Success: true, 39 | Required: []string{"reg-*-edt@app.idntimes.com"}, 40 | Supplied: []string{"reg-sul-edt@app.idntimes.com", "reg--edt@app.idntimes.com"}, 41 | }, { 42 | Success: false, 43 | Required: []string{"reg-*-edt@app.idntimes.com"}, 44 | Supplied: []string{"reg-sul-wow@app.idntimes.com", "reg--edt@app.idntimes.com"}, 45 | }, { 46 | Success: false, 47 | Required: []string{"basic@app.idntimes.com"}, 48 | Supplied: []string{}, 49 | }, { 50 | Success: true, 51 | Required: []string{}, 52 | Supplied: []string{"basic@app.idntimes.com"}, 53 | }, { 54 | Success: true, 55 | Required: []string{"basic@app.idntimes.com"}, 56 | Supplied: []string{"basic@app.idntimes.com"}, 57 | }, { 58 | Success: true, 59 | Required: []string{"*@app.idntimes.com"}, 60 | Supplied: []string{"basic@app.idntimes.com"}, 61 | }, { 62 | Success: true, 63 | Required: []string{"basic@app.idntimes.com"}, 64 | Supplied: []string{"*@app.idntimes.com"}, 65 | }, { 66 | Success: true, 67 | Required: []string{"basic@app.idntimes.com", "admin@app.idntimes.com"}, 68 | Supplied: []string{"*@app.idntimes.com"}, 69 | }, { 70 | Success: true, 71 | Required: []string{"*@app.idntimes.com"}, 72 | Supplied: []string{"basic@app.idntimes.com", "admin@app.idntimes.com"}, 73 | }, { 74 | Success: false, 75 | Required: []string{"*@app.idntimes.com"}, 76 | Supplied: []string{"basic@popmama.com", "admin@popmama.com"}, 77 | }, { 78 | Success: true, 79 | Required: []string{"basic@app.idntimes.com"}, 80 | Supplied: []string{"basic@popmama.com", "*@app.idntimes.com"}, 81 | }, { 82 | Success: true, 83 | Required: []string{"basic@app-fuse.idntimes.com"}, 84 | Supplied: []string{"basic@popmama.com", "basic@*.idntimes.com"}, 85 | }, { 86 | Success: true, 87 | Required: []string{"basic@app.idntimes.com"}, 88 | Supplied: []string{"basic@popmama.com", "basic@*.idntimes.com"}, 89 | }, { 90 | Success: true, 91 | Required: []string{"basic@app.idntimes.com"}, 92 | Supplied: []string{"basic@popmama.com", "basic@*.com"}, 93 | }, { 94 | Success: true, 95 | Required: []string{"basic@app.idntimes.com", "admin@app.idntimes.com", "abc@popmama.com"}, 96 | Supplied: []string{"*@*"}, 97 | }, { 98 | Success: true, 99 | Required: []string{"*@*"}, 100 | Supplied: []string{"basic@app.idntimes.com", "admin@app.idntimes.com", "abc@popmama.com"}, 101 | }, { 102 | Success: false, 103 | Required: []string{"*"}, 104 | Supplied: []string{"basic@app.idntimes.com", "admin@app.idntimes.com", "abc@popmama.com"}, 105 | }, { 106 | Success: false, 107 | Required: []string{"basic@app.idntimes.com", "admin@app.idntimes.com", "abc@popmama.com"}, 108 | Supplied: []string{"*"}, 109 | }, { 110 | Success: false, 111 | Required: []string{"basic@app.idntimes.com", "admin@app.idntimes.com"}, 112 | Supplied: []string{"basic@app.idntimes.com", "abc@app.idntimes.com", "cde@app.idntimes.com", "efg@app.idntimes.com"}, 113 | }, 114 | } 115 | 116 | for i, td := range TestData { 117 | if td.Success != IsRoleValid(td.Required, td.Supplied) { 118 | t.Logf("Test %d expect is %v but %v", i, td.Success, IsRoleValid(td.Required, td.Supplied)) 119 | t.Fail() 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /internal/mailer/Mailer.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "context" 5 | "github.com/hyperjumptech/hansip/internal/config" 6 | "github.com/hyperjumptech/hansip/internal/connector" 7 | "github.com/hyperjumptech/hansip/internal/constants" 8 | log "github.com/sirupsen/logrus" 9 | "io/ioutil" 10 | "net/url" 11 | "strings" 12 | "text/template" 13 | ) 14 | 15 | var ( 16 | mailerLogger = log.WithField("go", "Mailer") 17 | 18 | // MailerChannel mailer channel to receive new mail to send. 19 | MailerChannel chan *Email 20 | 21 | // KillChannel a bolean channel to detect mailer server shutdown 22 | KillChannel chan bool 23 | 24 | // Sender the connector used in this mailer 25 | Sender connector.EmailSender 26 | 27 | // Templates maps list of email template to use 28 | Templates map[string]*EmailTemplates 29 | ) 30 | 31 | // Email contains data structure of a new email 32 | type Email struct { 33 | context context.Context 34 | From string 35 | FromName string 36 | To []string 37 | Cc []string 38 | Bcc []string 39 | Template string 40 | Data interface{} 41 | } 42 | 43 | // TemplateLoader will load from specified resourceURI. 44 | // if the specified resource URI is not valid, it will return the resource in the parameter. 45 | // if the specified resource URI is valid, it will load the specified file and return its content. 46 | func TemplateLoader(resourceURI string) (string, error) { 47 | url, err := url.ParseRequestURI(resourceURI) 48 | if err != nil { 49 | return resourceURI, nil 50 | } 51 | filePath := url.Path 52 | content, err := ioutil.ReadFile(filePath) 53 | if err != nil { 54 | return "", err 55 | } 56 | return string(content), nil 57 | } 58 | 59 | // EmailTemplates data structure for an email template 60 | type EmailTemplates struct { 61 | SubjectTemplate *template.Template 62 | BodyTemplate *template.Template 63 | } 64 | 65 | func parseTemplate(name, text string) *template.Template { 66 | tmpl, err := template.New(name).Parse(text) 67 | if err != nil { 68 | panic(err) 69 | } 70 | return tmpl 71 | } 72 | 73 | func init() { 74 | MailerChannel = make(chan *Email) 75 | KillChannel = make(chan bool) 76 | Templates = make(map[string]*EmailTemplates) 77 | 78 | emailVeriSubTempl, err := TemplateLoader(config.Get("mailer.templates.emailveri.subject")) 79 | if err != nil { 80 | panic(err.Error()) 81 | } 82 | emailVeriBodTempl, err := TemplateLoader(config.Get("mailer.templates.emailveri.body")) 83 | if err != nil { 84 | panic(err.Error()) 85 | } 86 | 87 | emailPassRecSubTempl, err := TemplateLoader(config.Get("mailer.templates.passrecover.subject")) 88 | if err != nil { 89 | panic(err.Error()) 90 | } 91 | 92 | emailPassRecBodTempl, err := TemplateLoader(config.Get("mailer.templates.passrecover.body")) 93 | if err != nil { 94 | panic(err.Error()) 95 | } 96 | 97 | Templates["EMAIL_VERIFY"] = &EmailTemplates{ 98 | SubjectTemplate: parseTemplate("verifySubject", emailVeriSubTempl), 99 | BodyTemplate: parseTemplate("verifyBody", emailVeriBodTempl), 100 | } 101 | Templates["PASSPHRASE_RECOVERY"] = &EmailTemplates{ 102 | SubjectTemplate: parseTemplate("passRecoverSubject", emailPassRecSubTempl), 103 | BodyTemplate: parseTemplate("passRecoverBody", emailPassRecBodTempl), 104 | } 105 | 106 | } 107 | 108 | // Start will start this mailer server 109 | func Start() { 110 | mailerLogger.Info("Mailer starting") 111 | running := true 112 | for running { 113 | select { 114 | case mail := <-MailerChannel: 115 | fLog := mailerLogger.WithField("RequestID", mail.context.Value(constants.RequestID)) 116 | if Sender == nil { 117 | fLog.Errorf("not sent because mail Sender is nil") 118 | } else { 119 | if templates, ok := Templates[mail.Template]; ok { 120 | subjectWriter := &strings.Builder{} 121 | err := templates.SubjectTemplate.Execute(subjectWriter, mail.Data) 122 | if err != nil { 123 | fLog.Errorf("templates.SubjectTemplate.Execute got %s", err.Error()) 124 | } 125 | bodyWriter := &strings.Builder{} 126 | err = templates.BodyTemplate.Execute(bodyWriter, mail.Data) 127 | if err != nil { 128 | fLog.Errorf("templates.BodyTemplate.Execute got %s", err.Error()) 129 | } 130 | err = Sender.SendEmail(mail.context, mail.To, mail.Cc, mail.Bcc, mail.From, mail.FromName, subjectWriter.String(), bodyWriter.String()) 131 | if err != nil { 132 | fLog.Errorf("Sender.SendEmail got %s", err.Error()) 133 | } 134 | fLog.Tracef("email sent to %s", mail.To) 135 | } else { 136 | fLog.Errorf("not sent because mail template not recognized %s", mail.Template) 137 | } 138 | } 139 | case stop := <-KillChannel: 140 | if stop { 141 | running = false 142 | break 143 | } 144 | } 145 | } 146 | mailerLogger.Info("Mailer stopped") 147 | } 148 | 149 | // Send will add an email to the channel for sending. 150 | func Send(context context.Context, mail *Email) { 151 | mail.context = context 152 | MailerChannel <- mail 153 | } 154 | 155 | // Stop stop the channel 156 | func Stop() { 157 | KillChannel <- true 158 | } 159 | -------------------------------------------------------------------------------- /internal/config/Configuration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "github.com/spf13/viper" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | defCfg map[string]string 12 | initialized = false 13 | ) 14 | 15 | // initialize this configuration 16 | func initialize() { 17 | viper.SetEnvPrefix("aaa") 18 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 19 | viper.AutomaticEnv() 20 | defCfg = make(map[string]string) 21 | 22 | defCfg["api.path.prefix"] = "/api/v1" 23 | 24 | defCfg["server.host"] = "localhost" 25 | defCfg["server.port"] = "3000" 26 | defCfg["server.log.level"] = "warn" // valid values are trace, debug, info, warn, error, fatal 27 | defCfg["server.timeout.write"] = "15 seconds" 28 | defCfg["server.timeout.read"] = "15 seconds" 29 | defCfg["server.timeout.idle"] = "60 seconds" 30 | defCfg["server.timeout.graceshut"] = "15 seconds" 31 | defCfg["server.http.cors.enable"] = "true" 32 | defCfg["server.http.cors.allow.origins"] = "*" 33 | defCfg["server.http.cors.allow.credential"] = "true" 34 | defCfg["server.http.cors.allow.method"] = "GET,PUT,DELETE,POST,OPTIONS" 35 | defCfg["server.http.cors.allow.headers"] = "Accept,Authorization,Content-Type,X-CSRF-TOKEN,Accept-Encoding,X-Forwarded-For,X-Real-IP,X-Request-ID" 36 | defCfg["server.http.cors.exposed.headers"] = "*" 37 | defCfg["server.http.cors.optionpassthrough"] = "true" 38 | defCfg["server.http.cors.maxage"] = "300" 39 | 40 | defCfg["token.issuer"] = "aaa.domain.com" 41 | defCfg["token.access.duration"] = "5 minutes" 42 | defCfg["token.refresh.duration"] = "1 year" 43 | 44 | defCfg["token.crypt.key"] = "th15mustb3CH@ngedINprodUCT10N" 45 | defCfg["token.crypt.method"] = "HS512" 46 | 47 | defCfg["db.type"] = "MYSQL" // MYSQL, SQLITE 48 | defCfg["db.mysql.host"] = "localhost" 49 | defCfg["db.mysql.port"] = "3306" 50 | defCfg["db.mysql.user"] = "devuser" 51 | defCfg["db.mysql.password"] = "devpassword" 52 | defCfg["db.mysql.database"] = "devdb" 53 | 54 | defCfg["db.pool.maxidle"] = "3" 55 | defCfg["db.pool.maxopen"] = "10" 56 | 57 | defCfg["hansip.domain"] = "hansip" 58 | defCfg["hansip.admin"] = "admin" 59 | 60 | defCfg["security.passphrase.minchars"] = "8" 61 | defCfg["security.passphrase.minwords"] = "3" 62 | defCfg["security.passphrase.mincharsinword"] = "3" 63 | 64 | defCfg["mailer.type"] = "SENDGRID" // DUMMY, SENDMAIL, SENDGRID 65 | defCfg["mailer.from"] = "hansip@aaa.com" 66 | defCfg["mailer.from.name"] = "hansip@aaa.com" 67 | defCfg["mailer.sendmail.host"] = "localhost" 68 | defCfg["mailer.sendmail.port"] = "25" 69 | defCfg["mailer.sendmail.user"] = "sendmail" 70 | defCfg["mailer.sendmail.password"] = "password" 71 | defCfg["mailer.templates.emailveri.subject"] = "Please verify your new Hansip account's email" 72 | defCfg["mailer.templates.emailveri.body"] = "Dear New Hansip User

Your new account is ready!
please click this link to activate your account.

Cordially,
HANSIP team" 73 | defCfg["mailer.templates.passrecover.subject"] = "Passphrase recovery instruction" 74 | defCfg["mailer.templates.passrecover.body"] = "Dear Hansip User

To recover your passphrase
please click this link to change your passphrase.

Cordially,
HANSIP team" 75 | defCfg["mailer.sendgrid.token"] = "SENDGRIDTOKEN" 76 | 77 | for k := range defCfg { 78 | err := viper.BindEnv(k) 79 | if err != nil { 80 | log.Errorf("Failed to bind env \"%s\" into configuration. Got %s", k, err) 81 | } 82 | } 83 | 84 | initialized = true 85 | } 86 | 87 | // SetConfig put configuration key value 88 | func SetConfig(key, value string) { 89 | viper.Set(key, value) 90 | } 91 | 92 | // Get fetch configuration as string value 93 | func Get(key string) string { 94 | if !initialized { 95 | initialize() 96 | } 97 | ret := viper.GetString(key) 98 | if len(ret) == 0 { 99 | if ret, ok := defCfg[key]; ok { 100 | return ret 101 | } 102 | log.Debugf("%s config key not found", key) 103 | } 104 | return ret 105 | } 106 | 107 | // GetBoolean fetch configuration as boolean value 108 | func GetBoolean(key string) bool { 109 | if len(Get(key)) == 0 { 110 | return false 111 | } 112 | b, err := strconv.ParseBool(Get(key)) 113 | if err != nil { 114 | panic(err) 115 | } 116 | return b 117 | } 118 | 119 | // GetInt fetch configuration as integer value 120 | func GetInt(key string) int { 121 | if len(Get(key)) == 0 { 122 | return 0 123 | } 124 | i, err := strconv.ParseInt(Get(key), 10, 64) 125 | if err != nil { 126 | panic(err) 127 | } 128 | return int(i) 129 | } 130 | 131 | // GetFloat fetch configuration as float value 132 | func GetFloat(key string) float64 { 133 | if len(Get(key)) == 0 { 134 | return 0 135 | } 136 | f, err := strconv.ParseFloat(Get(key), 64) 137 | if err != nil { 138 | panic(err) 139 | } 140 | return f 141 | } 142 | 143 | // Set configuration key value 144 | func Set(key, value string) { 145 | defCfg[key] = value 146 | } 147 | -------------------------------------------------------------------------------- /internal/endpoint/Recovery.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hyperjumptech/hansip/internal/config" 7 | "github.com/hyperjumptech/hansip/internal/constants" 8 | "github.com/hyperjumptech/hansip/internal/mailer" 9 | "github.com/hyperjumptech/hansip/internal/passphrase" 10 | "github.com/hyperjumptech/hansip/pkg/helper" 11 | log "github.com/sirupsen/logrus" 12 | "golang.org/x/crypto/bcrypt" 13 | "io/ioutil" 14 | "net/http" 15 | ) 16 | 17 | var ( 18 | recoveryLogger = log.WithField("go", "Recovery") 19 | ) 20 | 21 | // RecoverPassphraseRequest hold the model for requesting passphrase recovery 22 | type RecoverPassphraseRequest struct { 23 | Email string `json:"email"` 24 | } 25 | 26 | // RecoverPassphrase serving request for recovering passphrase 27 | func RecoverPassphrase(w http.ResponseWriter, r *http.Request) { 28 | fLog := recoveryLogger.WithField("func", "RecoverPassphrase").WithField("RequestID", r.Context().Value(constants.RequestID)).WithField("path", r.URL.Path).WithField("method", r.Method) 29 | req := &RecoverPassphraseRequest{} 30 | body, err := ioutil.ReadAll(r.Body) 31 | if err != nil { 32 | fLog.Errorf("ioutil.ReadAll got %s", err.Error()) 33 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 34 | return 35 | } 36 | err = json.Unmarshal(body, req) 37 | if err != nil { 38 | fLog.Errorf("json.Unmarshal got %s", err.Error()) 39 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 40 | return 41 | } 42 | user, err := UserRepo.GetUserByEmail(r.Context(), req.Email) 43 | if err != nil { 44 | fLog.Errorf("UserRepo.GetUserByEmail got %s", err.Error()) 45 | // send fake success 46 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Check your email", nil, nil) 47 | return 48 | } 49 | if user == nil { 50 | // send fake success 51 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Check your email", nil, nil) 52 | return 53 | } 54 | user.RecoveryCode = helper.MakeRandomString(10, true, true, true, false) 55 | UserRepo.UpdateUser(r.Context(), user) 56 | 57 | fLog.Warnf("Sending email") 58 | mailer.Send(r.Context(), &mailer.Email{ 59 | From: config.Get("mailer.from"), 60 | FromName: config.Get("mailer.from.name"), 61 | To: []string{user.Email}, 62 | Cc: nil, 63 | Bcc: nil, 64 | Template: "PASSPHRASE_RECOVERY", 65 | Data: user, 66 | }) 67 | 68 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Check your email", nil, nil) 69 | } 70 | 71 | // ResetPassphraseRequest hold data model for reseting passphrase 72 | type ResetPassphraseRequest struct { 73 | ResetToken string `json:"passphraseResetToken"` 74 | NewPassphrase string `json:"newPassphrase"` 75 | } 76 | 77 | // ResetPassphrase serving passphrase reset request 78 | func ResetPassphrase(w http.ResponseWriter, r *http.Request) { 79 | fLog := recoveryLogger.WithField("func", "ResetPassphrase").WithField("RequestID", r.Context().Value(constants.RequestID)).WithField("path", r.URL.Path).WithField("method", r.Method) 80 | req := &ResetPassphraseRequest{} 81 | body, err := ioutil.ReadAll(r.Body) 82 | if err != nil { 83 | fLog.Errorf("ioutil.ReadAll got %s", err.Error()) 84 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 85 | return 86 | } 87 | err = json.Unmarshal(body, req) 88 | if err != nil { 89 | fLog.Errorf("json.Unmarshal got %s", err.Error()) 90 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 91 | return 92 | } 93 | isValidPassphrase := passphrase.Validate(req.NewPassphrase, config.GetInt("security.passphrase.minchars"), config.GetInt("security.passphrase.minwords"), config.GetInt("security.passphrase.mincharsinword")) 94 | if !isValidPassphrase { 95 | fLog.Errorf("Passphrase invalid") 96 | invalidMsg := fmt.Sprintf("Invalid passphrase. Passphrase must at least has %d characters and %d words and for each word have minimum %d characters", config.GetInt("security.passphrase.minchars"), config.GetInt("security.passphrase.minwords"), config.GetInt("security.passphrase.mincharsinword")) 97 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, "invalid passphrase", nil, invalidMsg) 98 | return 99 | } 100 | 101 | user, err := UserRepo.GetUserByRecoveryToken(r.Context(), req.ResetToken) 102 | if err != nil { 103 | fLog.Errorf("UserRepo.GetUserByRecoveryToken got %s", err.Error()) 104 | // send fake response 105 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Check your email", nil, nil) 106 | return 107 | } 108 | if user == nil { 109 | // send fake response 110 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Check your email", nil, nil) 111 | return 112 | } 113 | pass, err := bcrypt.GenerateFromPassword([]byte(req.NewPassphrase), 14) 114 | if err != nil { 115 | fLog.Errorf("bcrypt.GenerateFromPassword got %s", err.Error()) 116 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 117 | return 118 | } 119 | user.HashedPassphrase = string(pass) 120 | UserRepo.UpdateUser(r.Context(), user) 121 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Passphrase changed", nil, nil) 122 | } 123 | -------------------------------------------------------------------------------- /internal/connector/EmailSendConnector.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "github.com/sendgrid/sendgrid-go" 8 | "github.com/sendgrid/sendgrid-go/helpers/mail" 9 | "github.com/sirupsen/logrus" 10 | "net/smtp" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | mailerLog = logrus.WithField("system", "mailer") 16 | ) 17 | 18 | const ( 19 | mime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" 20 | ) 21 | 22 | // EmailSender an email sender interface 23 | type EmailSender interface { 24 | SendEmail(ctx context.Context, to, cc, bcc []string, from, fromName, subject, body string) error 25 | } 26 | 27 | // Recipients contains recipient map 28 | type Recipients struct { 29 | To map[string]bool 30 | } 31 | 32 | // AddAll adds multiple recipient in array 33 | func (r *Recipients) AddAll(re []string) { 34 | for _, ri := range re { 35 | r.To[ri] = true 36 | } 37 | } 38 | 39 | // Recipients returns all recipients 40 | func (r *Recipients) Recipients() []string { 41 | ret := make([]string, 0) 42 | for k := range r.To { 43 | ret = append(ret, k) 44 | } 45 | return ret 46 | } 47 | 48 | // DummyMailSender a dummy email sender. It does not send any email. 49 | type DummyMailSender struct { 50 | LastSentMail *DummyMail 51 | } 52 | 53 | // DummyMail dummy email data structure 54 | type DummyMail struct { 55 | From string 56 | To string 57 | Cc string 58 | Bcc string 59 | Subject string 60 | Body string 61 | } 62 | 63 | // SendEmail a dummy implementation, it just log out the email information. 64 | func (sender *DummyMailSender) SendEmail(ctx context.Context, to, cc, bcc []string, from, fromName, subject, body string) error { 65 | sender.LastSentMail = &DummyMail{ 66 | From: from, 67 | Subject: subject, 68 | Body: body, 69 | } 70 | if to != nil { 71 | sender.LastSentMail.To = strings.Join(to, ",") 72 | } 73 | if cc != nil { 74 | sender.LastSentMail.Cc = strings.Join(cc, ",") 75 | } 76 | if bcc != nil { 77 | sender.LastSentMail.Bcc = strings.Join(bcc, ",") 78 | } 79 | return nil 80 | } 81 | 82 | // SendMailSender send mail implementation using sendmail 83 | type SendMailSender struct { 84 | Host string 85 | Port int 86 | User string 87 | Password string 88 | } 89 | 90 | // SendEmail implementation to send email using sendmail 91 | func (sender *SendMailSender) SendEmail(ctx context.Context, to, cc, bcc []string, from, fromName, subject, body string) error { 92 | 93 | auth := smtp.PlainAuth("", sender.User, sender.Password, sender.Host) 94 | rec := &Recipients{ 95 | To: make(map[string]bool), 96 | } 97 | var bodyBuffer bytes.Buffer 98 | if to != nil && len(to) > 0 { 99 | rec.AddAll(to) 100 | bodyBuffer.WriteString("To: ") 101 | bodyBuffer.WriteString(strings.Join(to, ",")) 102 | bodyBuffer.WriteString("\r\n") 103 | } 104 | 105 | if cc != nil && len(cc) > 0 { 106 | rec.AddAll(cc) 107 | bodyBuffer.WriteString("Cc: ") 108 | bodyBuffer.WriteString(strings.Join(cc, ",")) 109 | bodyBuffer.WriteString("\r\n") 110 | } 111 | if bcc != nil && len(bcc) > 0 { 112 | rec.AddAll(bcc) 113 | bodyBuffer.WriteString("\r\n") 114 | } 115 | bodyBuffer.WriteString("Subject: ") 116 | bodyBuffer.WriteString(subject) 117 | bodyBuffer.WriteString("\r\n\r\n") 118 | bodyBuffer.WriteString(mime) 119 | bodyBuffer.WriteString("\r\n") 120 | bodyBuffer.WriteString(body) 121 | 122 | sendmailLog := mailerLog.WithField("mailer", "sendmail").WithField("mailto", strings.Join(to, ",")) 123 | 124 | err := smtp.SendMail(fmt.Sprintf("%s:%d", sender.Host, sender.Port), auth, from, rec.Recipients(), bodyBuffer.Bytes()) 125 | if err != nil { 126 | sendmailLog.Error(err) 127 | return err 128 | } 129 | sendmailLog.Debug("send mail success") 130 | return nil 131 | } 132 | 133 | // SendGridSender implementation using sendgrid. contains sendgrid token. 134 | type SendGridSender struct { 135 | Token string 136 | } 137 | 138 | // getMailBoxName get the mailbox portion of an email. abc@domain.com will return "abc" 139 | func getMailBoxName(email string) string { 140 | if strings.Index(email, "@") > 0 { 141 | return email[:strings.Index(email, "@")] 142 | } 143 | return email 144 | } 145 | 146 | // SendEmail email sending implementation using SendGrid 147 | func (sender *SendGridSender) SendEmail(ctx context.Context, to, cc, bcc []string, from, fromName, subject, body string) error { 148 | sendGridMail := mail.NewV3Mail() 149 | 150 | persona := mail.NewPersonalization() 151 | if to != nil { 152 | for _, t := range to { 153 | persona.AddTos(mail.NewEmail(getMailBoxName(t), t)) 154 | } 155 | } 156 | if cc != nil { 157 | for _, t := range cc { 158 | persona.AddCCs(mail.NewEmail(getMailBoxName(t), t)) 159 | } 160 | } 161 | if bcc != nil { 162 | for _, t := range bcc { 163 | persona.AddBCCs(mail.NewEmail(getMailBoxName(t), t)) 164 | } 165 | } 166 | persona.Subject = subject 167 | sendGridMail.AddPersonalizations(persona) 168 | 169 | content := mail.NewContent("text/html", body) 170 | sendGridMail.AddContent(content) 171 | 172 | sendGridMail.SetFrom(mail.NewEmail(fromName, from)) 173 | 174 | if len(sender.Token) == 0 { 175 | panic("sendgrid mailer with no token configured") 176 | } 177 | 178 | sendGridClient := sendgrid.NewSendClient(sender.Token) 179 | resp, err := sendGridClient.Send(sendGridMail) 180 | sendgridLog := mailerLog.WithField("mailer", "sendgrid").WithField("mailto", strings.Join(to, ",")) 181 | if err != nil { 182 | sendgridLog.Errorf("error while sending email. got %s", err.Error()) 183 | return err 184 | } 185 | sendgridLog.Debugf("response status %d, body %s", resp.StatusCode, resp.Body) 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /internal/endpoint/Endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "regexp" 7 | "strings" 8 | "time" 9 | 10 | "github.com/hyperjumptech/hansip/internal/config" 11 | "github.com/hyperjumptech/hansip/internal/hansiperrors" 12 | "github.com/hyperjumptech/hansip/pkg/helper" 13 | ) 14 | 15 | const ( 16 | // OptionMethod flag 17 | OptionMethod = 0b00000001 18 | // HeadMethod flag 19 | HeadMethod = 0b00000010 20 | // GetMethod flag 21 | GetMethod = 0b00000100 22 | // PostMethod flag 23 | PostMethod = 0b00001000 24 | // PutMethod flag 25 | PutMethod = 0b00010000 26 | // PatchMethod flag 27 | PatchMethod = 0b00100000 28 | // DeleteMethod flag 29 | DeleteMethod = 0b01000000 30 | ) 31 | 32 | var ( 33 | search = regexp.MustCompile(`\{[a-zA-Z0-9_]+\}`) 34 | ) 35 | 36 | // Endpoint struct for the route table 37 | type Endpoint struct { 38 | PathPattern string 39 | AllowedMethodFlag uint8 40 | IsPublic bool 41 | WhiteListAudiences []string 42 | HandleFunction func(http.ResponseWriter, *http.Request) 43 | } 44 | 45 | // GetMethodFlag decods Method string to Method flag 46 | func GetMethodFlag(method string) uint8 { 47 | switch strings.ToUpper(method) { 48 | case "OPTIONS": 49 | return OptionMethod 50 | case "HEAD": 51 | return HeadMethod 52 | case "GET": 53 | return GetMethod 54 | case "POST": 55 | return PostMethod 56 | case "PUT": 57 | return PutMethod 58 | case "PATCH": 59 | return PatchMethod 60 | case "DELETE": 61 | return DeleteMethod 62 | } 63 | return 0 64 | } 65 | 66 | func (e *Endpoint) getPathGob() string { 67 | return search.ReplaceAllString(e.PathPattern, "*") 68 | } 69 | 70 | func (e *Endpoint) isPathCanAccess(path string) bool { 71 | match, err := helper.Match(e.getPathGob(), path) 72 | if err != nil { 73 | return false 74 | } 75 | return match 76 | } 77 | 78 | func getHToken(r *http.Request) (*helper.HansipToken, error) { 79 | // If it need validation, Check the Authorization header 80 | authHeader := r.Header.Get("Authorization") 81 | if len(authHeader) == 0 { 82 | return nil, &hansiperrors.ErrMissingAuthorizationHeader{} 83 | } 84 | meth := strings.ToLower(strings.TrimSpace(authHeader[:6])) 85 | if meth != "bearer" { 86 | return nil, &hansiperrors.ErrInvalidAuthorizationMethod{} 87 | } 88 | // Get the token, validate and parse it. 89 | tok := strings.TrimSpace(authHeader[7:]) 90 | hToken, err := TokenFactory.ReadToken(tok) 91 | if err != nil { 92 | return nil, &hansiperrors.ErrTokenInvalid{Wrapped: err} 93 | } 94 | // Makesure the issuer is the same 95 | if hToken.Issuer != config.Get("token.issuer") { 96 | return nil, &hansiperrors.ErrInvalidIssuer{InvalidIssuer: hToken.Issuer} 97 | } 98 | return hToken, err 99 | } 100 | 101 | // AccessValid header tokens 102 | func (e *Endpoint) AccessValid(r *http.Request, TokenFactory helper.TokenFactory) (*helper.HansipToken, error) { 103 | path := r.URL.Path 104 | method := GetMethodFlag(r.Method) 105 | hTok, hTokErr := getHToken(r) 106 | if e.IsPublic { 107 | if hTokErr != nil { 108 | return &helper.HansipToken{ 109 | Issuer: config.Get("token.issuer"), 110 | Subject: "anonymous", 111 | Audiences: []string{"anonymous@*"}, 112 | Expire: time.Now().Add(24 * 360 * time.Hour), 113 | NotBefore: time.Time{}, 114 | IssuedAt: time.Time{}, 115 | Additional: map[string]interface{}{ 116 | "type": "access", 117 | }, 118 | }, e.canAccess(path, method, nil) 119 | } 120 | return hTok, e.canAccess(path, method, nil) 121 | } 122 | if hTokErr != nil { 123 | return nil, hTokErr 124 | } 125 | err := e.canAccess(path, method, hTok.Audiences) 126 | if err != nil { 127 | return nil, &hansiperrors.ErrTokenInvalid{Wrapped: err} 128 | } 129 | return hTok, nil 130 | 131 | } 132 | 133 | func (e *Endpoint) canAccess(path string, method uint8, audience []string) error { 134 | if !e.isPathCanAccess(path) { 135 | return &hansiperrors.ErrPathNotAllowed{ 136 | Given: path, 137 | Required: e.PathPattern, 138 | } 139 | } 140 | if method&e.AllowedMethodFlag != method { 141 | return &hansiperrors.ErrMethodNotAllowed{} 142 | } 143 | if e.IsPublic { 144 | return nil 145 | } 146 | if isRoleMatch(e.WhiteListAudiences, audience) { 147 | return nil 148 | } 149 | return &hansiperrors.ErrAudienceNotAllowed{Audiences: audience} 150 | } 151 | 152 | // FlagToListMethod convert back flags to http methods 153 | func FlagToListMethod(flags uint8) []string { 154 | methods := make([]string, 0) 155 | if flags&OptionMethod == OptionMethod { 156 | methods = append(methods, "OPTIONS") 157 | } 158 | if flags&HeadMethod == HeadMethod { 159 | methods = append(methods, "HEAD") 160 | } 161 | if flags&GetMethod == GetMethod { 162 | methods = append(methods, "GET") 163 | } 164 | if flags&PostMethod == PostMethod { 165 | methods = append(methods, "POST") 166 | } 167 | if flags&PutMethod == PutMethod { 168 | methods = append(methods, "PUT") 169 | } 170 | if flags&PatchMethod == PatchMethod { 171 | methods = append(methods, "PATCH") 172 | } 173 | if flags&DeleteMethod == DeleteMethod { 174 | methods = append(methods, "DELETE") 175 | } 176 | return methods 177 | } 178 | 179 | func isRoleMatch(needs, supplies []string) bool { 180 | for _, need := range needs { 181 | if need == "*@*" { 182 | return true 183 | } 184 | for _, supply := range supplies { 185 | if supply == "*@*" { 186 | return true 187 | } 188 | if strings.Contains(need, "*") && strings.Contains(supply, "*") { 189 | continue 190 | } else if strings.Contains(need, "*") { 191 | needPattern := fmt.Sprintf("^%s$", strings.ReplaceAll(need, "*", `.*`)) 192 | match, err := regexp.MatchString(needPattern, supply) 193 | if err == nil && match { 194 | return true 195 | } 196 | } else if strings.Contains(supply, "*") { 197 | supplyPattern := fmt.Sprintf("^%s$", strings.ReplaceAll(supply, "*", `.*`)) 198 | match, err := regexp.MatchString(supplyPattern, need) 199 | if err == nil && match { 200 | return true 201 | } 202 | } else if need == supply { 203 | return true 204 | } 205 | } 206 | } 207 | return false 208 | } 209 | -------------------------------------------------------------------------------- /pkg/store/cache/ObjectCache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // ObjectCache interface to define a standard CACHE interface. 10 | type ObjectCache interface { 11 | Store(key string, object interface{}) 12 | Fetch(key string) (bool, interface{}) 13 | KeysByPrefix(prefix string) []string 14 | Delete(key string) 15 | Clear() 16 | Size() int 17 | } 18 | 19 | // CacheItem is a linked list node. It maintain links up and down (equals to next and prev) 20 | type CacheItem struct { 21 | Key string 22 | up *CacheItem 23 | down *CacheItem 24 | Item interface{} 25 | createTime time.Time 26 | } 27 | 28 | // IsExpired check if this item is expired 29 | func (item *CacheItem) IsExpired(maxAgeSecond int) bool { 30 | dur := time.Now().Sub(item.createTime) 31 | return dur > (time.Duration(maxAgeSecond) * time.Second) 32 | } 33 | 34 | // NewInMemoryCache create new instance of InMemoryCache 35 | func NewInMemoryCache(capacity, ttlsecond int, ttlextend bool) ObjectCache { 36 | c := &InMemoryCache{ 37 | TTLSecond: ttlsecond, 38 | TTLExtend: ttlextend, 39 | Capacity: capacity, 40 | data: make(map[string]*CacheItem), 41 | top: nil, 42 | bottom: nil, 43 | } 44 | if c.Capacity < 10 { 45 | c.Capacity = 10 46 | } 47 | if c.TTLSecond < 1 { 48 | c.TTLSecond = 1 49 | } 50 | return c 51 | } 52 | 53 | // InMemoryCache is an implementation of Object cache 54 | type InMemoryCache struct { 55 | mutex sync.Mutex 56 | TTLSecond int 57 | TTLExtend bool 58 | Capacity int 59 | data map[string]*CacheItem 60 | top *CacheItem 61 | bottom *CacheItem 62 | } 63 | 64 | // Size returns total number of valid cached object. 65 | func (cache *InMemoryCache) Size() int { 66 | cache.mutex.Lock() 67 | defer cache.mutex.Unlock() 68 | cache.removeExpired() 69 | return len(cache.data) 70 | } 71 | 72 | // Store saves object into this cache 73 | func (cache *InMemoryCache) Store(key string, object interface{}) { 74 | cache.mutex.Lock() 75 | defer cache.mutex.Unlock() 76 | if item, exist := cache.data[key]; exist { // Replacing 77 | i := &CacheItem{ 78 | Key: key, 79 | up: item.up, 80 | down: item.down, 81 | Item: object, 82 | createTime: time.Now(), 83 | } 84 | if item.up != nil { 85 | item.up.down = i 86 | } 87 | if item.down != nil { 88 | item.down.up = i 89 | } 90 | cache.data[key] = i 91 | } else { // Inserting 92 | i := &CacheItem{ 93 | Key: key, 94 | up: nil, 95 | down: nil, 96 | Item: object, 97 | createTime: time.Now(), 98 | } 99 | if cache.top != nil { 100 | cache.top.up = i 101 | i.down = cache.top 102 | } 103 | cache.top = i 104 | 105 | if cache.bottom == nil { 106 | cache.bottom = cache.top 107 | } 108 | cache.data[key] = i 109 | } 110 | // if the cache capacity is too long, cut the bottom most. 111 | for len(cache.data) > cache.Capacity { 112 | bottom := cache.bottom 113 | bottom.up.down = nil 114 | cache.bottom = bottom.up 115 | delete(cache.data, bottom.Key) 116 | } 117 | cache.removeExpired() 118 | } 119 | 120 | func (cache *InMemoryCache) removeExpired() { 121 | if cache.bottom == nil || !cache.bottom.IsExpired(cache.TTLSecond) { 122 | return 123 | } 124 | if cache.bottom == cache.top { 125 | delete(cache.data, cache.bottom.Key) 126 | cache.top = nil 127 | cache.bottom = nil 128 | return 129 | } 130 | for cache.bottom != nil && cache.bottom.IsExpired(cache.TTLSecond) { 131 | todel := cache.bottom 132 | cache.bottom = todel.up 133 | if cache.bottom != nil { 134 | cache.bottom.down = nil 135 | } 136 | delete(cache.data, todel.Key) 137 | todel = cache.bottom 138 | } 139 | } 140 | 141 | // Fetch valid objects from the cache. Valid means that its not yet expired. 142 | func (cache *InMemoryCache) Fetch(key string) (bool, interface{}) { 143 | cache.mutex.Lock() 144 | defer cache.mutex.Unlock() 145 | if data, ok := cache.data[key]; ok { 146 | if data.IsExpired(cache.TTLSecond) { 147 | cache.removeExpired() 148 | return false, nil 149 | } 150 | if cache.TTLExtend { 151 | data.createTime = time.Now() 152 | if data.up != nil { 153 | if data.down != nil { // we are in the middle 154 | // remove data from the linklist 155 | data.down.up = data.up 156 | data.up.down = data.down 157 | } else { // we are at the bottom 158 | // remove data from the linklist 159 | data.up.down = nil 160 | cache.bottom = data.up 161 | } 162 | // move to the top 163 | data.up = nil 164 | cache.top.up = data 165 | data.down = cache.top 166 | cache.top = data 167 | } 168 | } 169 | cache.removeExpired() 170 | return true, data.Item 171 | } 172 | return false, nil 173 | } 174 | 175 | // KeysByPrefix returns all valid cached key which have the specified prefix. 176 | func (cache *InMemoryCache) KeysByPrefix(prefix string) []string { 177 | cache.mutex.Lock() 178 | defer cache.mutex.Unlock() 179 | keys := make([]string, 0) 180 | cache.removeExpired() 181 | for k, _ := range cache.data { 182 | if strings.HasPrefix(k, prefix) { 183 | keys = append(keys, k) 184 | } 185 | if len(keys) > 100 { 186 | return keys 187 | } 188 | } 189 | return keys 190 | } 191 | 192 | // Delete a cached object by its key. 193 | func (cache *InMemoryCache) Delete(key string) { 194 | cache.mutex.Lock() 195 | defer cache.mutex.Unlock() 196 | if item, exist := cache.data[key]; exist { 197 | if item.up != nil { 198 | if item.down != nil { // we are in the middle 199 | item.up.down = item.down 200 | item.down.up = item.up 201 | } else { // we are at the bottom 202 | item.up.down = nil 203 | cache.bottom = item.up 204 | } 205 | } else { 206 | if item.down != nil { // we are at the top 207 | item.down.up = nil 208 | cache.top = item.down 209 | } else { // we are the only item. 210 | cache.bottom = nil 211 | cache.top = nil 212 | } 213 | } 214 | delete(cache.data, key) 215 | } 216 | } 217 | 218 | // Clear all objects from this cache 219 | func (cache *InMemoryCache) Clear() { 220 | cache.mutex.Lock() 221 | defer cache.mutex.Unlock() 222 | cache.data = make(map[string]*CacheItem) 223 | cache.top = nil 224 | cache.bottom = nil 225 | } 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hansip 2 | 3 | An AAA (Access Authentication & Authorization) Service by Hyperjump 4 | 5 | ## Building Hansip 6 | 7 | Prerequisites: 8 | 9 | 1. Golang 1.13 10 | 2. Make utility 11 | 12 | **Step 1 Checkout and Install Go-Resource** 13 | 14 | ```.bash 15 | $ git clone https://github.com/newm4n/go-resource.git 16 | $ cd go-resource 17 | $ go install 18 | ``` 19 | 20 | **Step 2 Checkout Hansip** 21 | 22 | ```.bash 23 | $ git clone https://github.com/hyperjumptech/hansip.git 24 | $ cd hansip 25 | ``` 26 | 27 | **Step 3 Build and Run** 28 | 29 | ```bash 30 | $ make build 31 | ``` 32 | 33 | Running the app will automatically build. 34 | 35 | ```bash 36 | $ make run 37 | ``` 38 | 39 | ## Testing Hansip 40 | 41 | ```bash 42 | $ make test 43 | ``` 44 | 45 | ## Configuring Hansip 46 | 47 | If you want to run Hansip from the make file using `make run` command, you have to 48 | modify the environment variable in the `run` phase. 49 | 50 | ```make 51 | run: build 52 | export AAA_SERVER_HOST=localhost; \ 53 | export AAA_SERVER_PORT=8088; \ 54 | export AAA_SETUP_ADMIN_ENABLE=true; \ 55 | ./$(IMAGE_NAME).app 56 | rm -f $(IMAGE_NAME).app 57 | ``` 58 | 59 | You can change the import env variable. 60 | 61 | If you're running from docker, you should modify the environment variable for the running 62 | image. 63 | 64 | ### Environment Variable Values 65 | 66 | | Variable | Environment Variable | Default | Description | 67 | | -------- | -------------------- | ------- | ----------- | 68 | | server.host| AAA_SERVER_HOST | localhost | The host name to bind. could be `localhost` or `0.0.0.0` | 69 | | server.port| AAA_SERVER_PORT | 3000 | The host port to listen from | 70 | | server.timeout.write| AAA_SERVER_TIMEOUT_WRITE | 15 seconds | Server write timeout | 71 | | server.timeout.read| AAA_SERVER_TIMEOUT_READ | 15 seconds | Server read timeout | 72 | | server.timeout.idle| AAA_SERVER_TIMEOUT_IDLE | 60 seconds | Server connection IDLE timeout | 73 | | server.timeout.graceshut| AAA_SERVER_TIMEOUT_GRACESHUT | 15 seconds | Server grace shutdown timeout | 74 | | setup.admin.enable| AAA_SETUP_ADMIN_ENABLE | false | Enable built in admin account | 75 | | setup.admin.email| AAA_SETUP_ADMIN_EMAIL |admin@hansip | Built in admin email address for authentication | 76 | | setup.admin.passphrase| AAA_SETUP_ADMIN_PASSPHRASE |this must be change in the production | Built in admin password for authentication | 77 | | token.issuer| AAA_TOKE_ISSUER |aaa.domain.com | JWT Token issuer value | 78 | | token.access.duration| AAA_ACCESS_DURATION |5 minutes | JWT Access token lifetime | 79 | | token.refresh.duration| AAA_REFRESH_DURATION |1 year | JWT Refresh token lifetime | 80 | | token.crypt.key| AAA_TOKEN_CRYPT_KEY |th15mustb3CH@ngedINprodUCT10N | JWT token crypto key | 81 | | token.crypt.method| AAA_TOKEN_CRYPT_METHOD |HS512 | JWT token crypto method | 82 | | db.type| AAA_DB_TYPE | INMEMORY | Database type. `INMEMORY` or `MYSQL` | 83 | | db.mysql.host| AAA_DB_MYSQL_HOST |localhost | MySQL host | 84 | | db.mysql.port| AAA_DB_MYSQL_PORT |3306 | MySQL Port | 85 | | db.mysql.user| AAA_DB_MYSQL_USER |user | MySQL User to login | 86 | | db.mysql.password| AAA_DB_MYSQL_PASSWORD |password | MySQL Password to login | 87 | | db.mysql.database| AAA_DB_MYSQL_DATABASE |hansip | MySQL Database to use | 88 | | db.mysql.maxidle| AAA_DB_MYSQL_MAXIDLE |3 | Maximum connection that can IDLE | 89 | | db.mysql.maxopen| AAA_DB_MYSQL_MAXOPEN |10 | Maximum open connection in the pool | 90 | | mailer.type| AAA_MAILER_TYPE | DUMMY | Mailer type. `DUMMY` or `SENDMAIL` | 91 | | mailer.from| AAA_MAILER_FROM |hansip@aaa.com | The email from field | 92 | | mailer.sendmail.host| AAA_MAILER_SENDMAIL_HOST |localhost | Mail server host | 93 | | mailer.sendmail.port| AAA_MAILER_SENDMAIL_PORT |25 | Mail server port | 94 | | mailer.sendmail.user| AAA_MAILER_SENDMAIL_USER |sendmail | Mail server user for authentication | 95 | | mailer.sendmail.password| AAA_MAILER_SENDMAIL_PASSWORD |password | Mail server password for authentication | 96 | | mailer.templates.emailveri.subject| AAA_MAILER_TEMPLATES_EMAILVERI_SUBJECT |Please verify your new Hansip account's email | Email verification subject template | 97 | | mailer.templates.emailveri.body| AAA_MAILER_TEMPLATES_EMAILVERI_BODY | `Dear New Hansip User

Your new account is ready!
please click this link to activate your account.

Cordially,
HANSIP team` | Email verification body template | 98 | | mailer.templates.passrecover.subject| AAA_MAILER_TEMPLATES_PASSRECOVER_SUBJECT | Passphrase recovery instruction | Password recovery email subject template | 99 | | mailer.templates.passrecover.body| AAA_MAILER_TEMPLATES_PASSRECOVER_BODY | `Dear Hansip User

To recover your passphrase
please click this link to change your passphrase.

Cordially,
HANSIP team` | Password recovery email body template | 100 | | server.http.cors.enable | AAA_SERVER_HTTP_CORS_ENABLE | true | To enable or disable CORS handling | 101 | | server.http.cors.allow.origins | AAA_SERVER_HTTP_CORS_ALLOW_ORIGINS | * | Indicates whether the response can be shared with requesting code from the given origin. | 102 | | server.http.cors.allow.credential | AAA_SERVER_HTTP_CORS_ALLOW_CREDENTIAL | true | response header tells browsers whether to expose the response to frontend JavaScript code when the request's credentials mode (`Request.credentials`) is `include` | 103 | | server.http.cors.allow.method | AAA_SERVER_HTTP_CORS_ALLOW_METHOD | GET,PUT,DELETE,POST,OPTIONS | response header specifies the method or methods allowed when accessing the resource in response to a preflight request. | 104 | | server.http.cors.allow.headers | AAA_SERVER_HTTP_CORS_ALLOW_HEADERS | Accept,Authorization,Content-Type,X-CSRF-TOKEN,Accept-Encoding,X-Forwarded-For,X-Real-IP,X-Request-ID | response header is used in response to a preflight request which includes the `Access-Control-Request-Headers` to indicate which HTTP headers can be used during the actual request. | 105 | | server.http.cors.exposed.headers | AAA_SERVER_HTTP_CORS_EXPOSED_HEADERS | * | response header indicates which headers can be exposed as part of the response by listing their names. | 106 | | server.http.cors.optionpassthrough | AAA_SERVER_HTTP_CORS_OPTIONPASSTHROUGH | true | Indicates that the OPTIONS method should be handled by server | 107 | | server.http.cors.maxage | AAA_SERVER_HTTP_CORS_MAXAGE | 300 | response header indicates how long the results of a preflight request (that is the information contained in the `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers) can be cached | 108 | 109 | ## API Doc 110 | 111 | After you have run the server, you can access the API Doc at 112 | 113 | [http://localhost:3000/docs/](http://localhost:3000/docs/) -------------------------------------------------------------------------------- /pkg/helper/role-check.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/helper/TokenFactory.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/SermoDigital/jose/crypto" 11 | "github.com/SermoDigital/jose/jws" 12 | ) 13 | 14 | type HansipToken struct { 15 | Issuer string 16 | Subject string 17 | Audiences []string 18 | Expire time.Time 19 | NotBefore time.Time 20 | IssuedAt time.Time 21 | Additional map[string]interface{} 22 | Token string 23 | } 24 | 25 | // TokenFactory defines a token factory function to implement 26 | type TokenFactory interface { 27 | CreateTokenPair(subject string, audience []string, additional map[string]interface{}) (string, string, error) 28 | ReadToken(token string) (*HansipToken, error) 29 | RefreshToken(refreshToken string) (string, error) 30 | } 31 | 32 | // NewTokenFactory create new instance of TokenFactory 33 | func NewTokenFactory(signKey, signMethod, issuer string, accessTokenAge, refreshTokenAge time.Duration) TokenFactory { 34 | if issuer == "" { 35 | panic("empty issuer") 36 | } 37 | return &DefaultTokenFactory{ 38 | Issuer: issuer, 39 | AccessTokenDuration: accessTokenAge, 40 | RefreshTokenDuration: refreshTokenAge, 41 | SignKey: signKey, 42 | SignMethod: signMethod, 43 | } 44 | } 45 | 46 | // DefaultTokenFactory default implementation of TokenFactory 47 | type DefaultTokenFactory struct { 48 | mutex sync.Mutex 49 | Issuer string 50 | AccessTokenDuration time.Duration 51 | RefreshTokenDuration time.Duration 52 | SignKey string 53 | SignMethod string 54 | } 55 | 56 | // CreateTokenPair create new Access and Refresh token pair 57 | func (tf *DefaultTokenFactory) CreateTokenPair(subject string, audience []string, additional map[string]interface{}) (string, string, error) { 58 | tf.mutex.Lock() 59 | defer tf.mutex.Unlock() 60 | accessAdditional := make(map[string]interface{}) 61 | refreshAdditional := make(map[string]interface{}) 62 | if additional != nil { 63 | for k, v := range additional { 64 | accessAdditional[k] = v 65 | refreshAdditional[k] = v 66 | } 67 | } 68 | accessAdditional["type"] = "access" 69 | refreshAdditional["type"] = "refresh" 70 | 71 | access, err := CreateJWTStringToken(tf.SignKey, tf.SignMethod, tf.Issuer, subject, audience, time.Now(), time.Now(), time.Now().Add(tf.AccessTokenDuration), accessAdditional) 72 | if err != nil { 73 | return "", "", err 74 | } 75 | refresh, err := CreateJWTStringToken(tf.SignKey, tf.SignMethod, tf.Issuer, subject, audience, time.Now(), time.Now(), time.Now().Add(tf.RefreshTokenDuration), refreshAdditional) 76 | if err != nil { 77 | return "", "", err 78 | } 79 | return access, refresh, nil 80 | } 81 | 82 | // ReadToken read a token string, validate and extract its content. 83 | func (tf *DefaultTokenFactory) ReadToken(token string) (*HansipToken, error) { 84 | issuer, subject, audience, issuedAt, notBefore, expire, additional, err := ReadJWTStringToken(true, tf.SignKey, tf.SignMethod, token) 85 | htoken := &HansipToken{ 86 | Issuer: issuer, 87 | Subject: subject, 88 | Audiences: audience, 89 | Expire: expire, 90 | NotBefore: notBefore, 91 | IssuedAt: issuedAt, 92 | Additional: additional, 93 | Token: token, 94 | } 95 | if issuer != tf.Issuer { 96 | return htoken, fmt.Errorf("invalid issuer %s", issuer) 97 | } 98 | return htoken, err 99 | } 100 | 101 | // RefreshToken generate new Access token by specifying its refresh token 102 | func (tf *DefaultTokenFactory) RefreshToken(refreshToken string) (string, error) { 103 | tf.mutex.Lock() 104 | defer tf.mutex.Unlock() 105 | hToken, err := tf.ReadToken(refreshToken) 106 | if err != nil { 107 | return "", err 108 | } 109 | if hToken.Issuer != tf.Issuer { 110 | return "", fmt.Errorf("invalid issuer") 111 | } 112 | if typ, ok := hToken.Additional["type"]; ok { 113 | if typ != "refresh" { 114 | return "", fmt.Errorf("not refresh token") 115 | } 116 | } else { 117 | return "", fmt.Errorf("unknown token type") 118 | } 119 | hToken.Additional["type"] = "access" 120 | access, err := CreateJWTStringToken(tf.SignKey, tf.SignMethod, tf.Issuer, hToken.Subject, hToken.Audiences, hToken.IssuedAt, hToken.NotBefore, time.Now().Add(tf.AccessTokenDuration), hToken.Additional) 121 | if err != nil { 122 | return "", err 123 | } 124 | return access, err 125 | } 126 | 127 | // ReadJWTStringToken takes a token string , keys, signMethod and returns its content. 128 | func ReadJWTStringToken(validate bool, signKey, signMethod, tokenString string) (string, string, []string, time.Time, time.Time, time.Time, map[string]interface{}, error) { 129 | if signKey == "th15mustb3CH@ngedINprodUCT10N" { 130 | logrus.Warnf("Using default CryptKey for JWT Token, This key is visible from the source tree and to be used in development only. YOU MUST CHANGE THIS IN PRODUCTION or TO REMOVE THIS LOG FROM APPEARING") 131 | } 132 | 133 | jwt, err := jws.ParseJWT([]byte(tokenString)) 134 | if err != nil { 135 | return "", "", nil, time.Now(), time.Now(), time.Now(), nil, fmt.Errorf("malformed jwt token") 136 | } 137 | 138 | if validate { 139 | var sMethod crypto.SigningMethod 140 | 141 | switch strings.ToUpper(signMethod) { 142 | case "HS256": 143 | sMethod = crypto.SigningMethodHS256 144 | case "HS384": 145 | sMethod = crypto.SigningMethodHS384 146 | case "HS512": 147 | sMethod = crypto.SigningMethodHS512 148 | default: 149 | sMethod = crypto.SigningMethodHS256 150 | } 151 | 152 | if err := jwt.Validate([]byte(signKey), sMethod); err != nil { 153 | return "", "", nil, time.Now(), time.Now(), time.Now(), nil, fmt.Errorf("invalid jwt token - %s", err.Error()) 154 | } 155 | } 156 | claims := jwt.Claims() 157 | additional := make(map[string]interface{}) 158 | for k, v := range claims { 159 | kup := strings.ToUpper(k) 160 | if kup != "ISS" && kup != "AUD" && kup != "SUB" && kup != "IAT" && kup != "EXP" && kup != "NBF" { 161 | additional[k] = v 162 | } 163 | } 164 | 165 | issuer, _ := claims.Issuer() 166 | subject, _ := claims.Subject() 167 | audience, _ := claims.Audience() 168 | expire, _ := claims.Expiration() 169 | notBefore, _ := claims.NotBefore() 170 | issuedAt, _ := claims.IssuedAt() 171 | 172 | return issuer, subject, audience, issuedAt, notBefore, expire, additional, nil 173 | } 174 | 175 | // CreateJWTStringToken create JWT String token based on arguments 176 | func CreateJWTStringToken(signKey, signMethod, issuer, subject string, audience []string, issuedAt, notBefore, expiration time.Time, additional map[string]interface{}) (string, error) { 177 | if signKey == "th15mustb3CH@ngedINprodUCT10N" { 178 | logrus.Warnf("Using default CryptKey for JWT Token, This key is visible from the source tree and to be used in development only. YOU MUST CHANGE THIS IN PRODUCTION or TO REMOVE THIS LOG FROM APPEARING") 179 | } 180 | 181 | claims := jws.Claims{} 182 | claims.SetIssuer(issuer) 183 | claims.SetSubject(subject) 184 | claims.SetAudience(audience...) 185 | claims.SetIssuedAt(issuedAt) 186 | claims.SetNotBefore(notBefore) 187 | claims.SetExpiration(expiration) 188 | 189 | for k, v := range additional { 190 | claims[k] = v 191 | } 192 | 193 | var signM crypto.SigningMethod 194 | 195 | switch strings.ToUpper(signMethod) { 196 | case "HS256": 197 | signM = crypto.SigningMethodHS256 198 | case "HS384": 199 | signM = crypto.SigningMethodHS384 200 | case "HS512": 201 | signM = crypto.SigningMethodHS512 202 | default: 203 | signM = crypto.SigningMethodHS256 204 | } 205 | 206 | jwtBytes := jws.NewJWT(claims, signM) 207 | 208 | tokenByte, err := jwtBytes.Serialize([]byte(signKey)) 209 | if err != nil { 210 | panic(err) 211 | } 212 | return string(tokenByte), nil 213 | } 214 | -------------------------------------------------------------------------------- /pkg/helper/Pagination_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | pages = []*Page{ 10 | { 11 | No: 1, 12 | TotalPages: 10, 13 | PageSize: 10, 14 | Items: 10, 15 | TotalItems: 100, 16 | HasNext: true, 17 | HasPrev: false, 18 | FistPage: 1, 19 | NextPage: 2, 20 | PrevPage: 1, 21 | LastPage: 10, 22 | IsFirst: true, 23 | IsLast: false, 24 | OrderBy: "ABC", 25 | OffsetStart: 0, 26 | OffsetEnd: 10, 27 | Sort: "ASC", 28 | }, 29 | { 30 | No: 2, 31 | TotalPages: 10, 32 | PageSize: 10, 33 | Items: 10, 34 | TotalItems: 100, 35 | HasNext: true, 36 | HasPrev: true, 37 | FistPage: 1, 38 | NextPage: 3, 39 | PrevPage: 1, 40 | LastPage: 10, 41 | IsFirst: false, 42 | IsLast: false, 43 | OrderBy: "ABC", 44 | OffsetStart: 10, 45 | OffsetEnd: 20, 46 | Sort: "ASC", 47 | }, 48 | { 49 | No: 1, 50 | TotalPages: 1, 51 | PageSize: 10, 52 | Items: 6, 53 | TotalItems: 6, 54 | HasNext: false, 55 | HasPrev: false, 56 | FistPage: 1, 57 | NextPage: 1, 58 | PrevPage: 1, 59 | LastPage: 1, 60 | IsFirst: true, 61 | IsLast: true, 62 | OrderBy: "ABC", 63 | OffsetStart: 0, 64 | OffsetEnd: 6, 65 | Sort: "ASC", 66 | }, 67 | { 68 | No: 1, 69 | TotalPages: 1, 70 | PageSize: 10, 71 | Items: 0, 72 | TotalItems: 0, 73 | HasNext: false, 74 | HasPrev: false, 75 | FistPage: 1, 76 | NextPage: 1, 77 | PrevPage: 1, 78 | LastPage: 1, 79 | IsFirst: true, 80 | IsLast: true, 81 | OrderBy: "ABC", 82 | OffsetStart: 0, 83 | OffsetEnd: 0, 84 | Sort: "ASC", 85 | }, 86 | { 87 | No: 1, 88 | TotalPages: 1, 89 | PageSize: 10, 90 | Items: 1, 91 | TotalItems: 1, 92 | HasNext: false, 93 | HasPrev: false, 94 | FistPage: 1, 95 | NextPage: 1, 96 | PrevPage: 1, 97 | LastPage: 1, 98 | IsFirst: true, 99 | IsLast: true, 100 | OrderBy: "ABC", 101 | OffsetStart: 0, 102 | OffsetEnd: 1, 103 | Sort: "ASC", 104 | }, 105 | { 106 | No: 1, 107 | TotalPages: 1, 108 | PageSize: 10, 109 | Items: 10, 110 | TotalItems: 10, 111 | HasNext: false, 112 | HasPrev: false, 113 | FistPage: 1, 114 | NextPage: 1, 115 | PrevPage: 1, 116 | LastPage: 1, 117 | IsFirst: true, 118 | IsLast: true, 119 | OrderBy: "ABC", 120 | OffsetStart: 0, 121 | OffsetEnd: 10, 122 | Sort: "ASC", 123 | }, 124 | { 125 | No: 1, 126 | TotalPages: 2, 127 | PageSize: 10, 128 | Items: 10, 129 | TotalItems: 11, 130 | HasNext: true, 131 | HasPrev: false, 132 | FistPage: 1, 133 | NextPage: 2, 134 | PrevPage: 1, 135 | LastPage: 2, 136 | IsFirst: true, 137 | IsLast: false, 138 | OrderBy: "ABC", 139 | OffsetStart: 0, 140 | OffsetEnd: 10, 141 | Sort: "ASC", 142 | }, 143 | { 144 | No: 2, 145 | TotalPages: 2, 146 | PageSize: 10, 147 | Items: 1, 148 | TotalItems: 11, 149 | HasNext: false, 150 | HasPrev: true, 151 | FistPage: 1, 152 | NextPage: 2, 153 | PrevPage: 1, 154 | LastPage: 2, 155 | IsFirst: false, 156 | IsLast: true, 157 | OrderBy: "ABC", 158 | OffsetStart: 10, 159 | OffsetEnd: 11, 160 | Sort: "ASC", 161 | }, 162 | { 163 | No: 5, 164 | TotalPages: 10, 165 | PageSize: 10, 166 | Items: 10, 167 | TotalItems: 100, 168 | HasNext: true, 169 | HasPrev: true, 170 | FistPage: 1, 171 | NextPage: 6, 172 | PrevPage: 4, 173 | LastPage: 10, 174 | IsFirst: false, 175 | IsLast: false, 176 | OrderBy: "ABC", 177 | OffsetStart: 40, 178 | OffsetEnd: 50, 179 | Sort: "ASC", 180 | }, 181 | { 182 | No: 11, 183 | TotalPages: 11, 184 | PageSize: 10, 185 | Items: 5, 186 | TotalItems: 105, 187 | HasNext: false, 188 | HasPrev: true, 189 | FistPage: 1, 190 | NextPage: 11, 191 | PrevPage: 10, 192 | LastPage: 11, 193 | IsFirst: false, 194 | IsLast: true, 195 | OrderBy: "ABC", 196 | OffsetStart: 100, 197 | OffsetEnd: 105, 198 | Sort: "ASC", 199 | }, 200 | } 201 | ) 202 | 203 | func TestNewPageRequestFromRequest(t *testing.T) { 204 | req := httptest.NewRequest("GET", "/?page_no=2&page_size=20&order_by=EMAIL&sort=DESC", nil) 205 | //req, err := http.NewRequest("GET", "/?page_no=2&page_size=20&order_by=EMAIL&sort=DESC", nil) 206 | t.Log(req.URL.String()) 207 | t.Log(req.URL.Query()) 208 | preq, err := NewPageRequestFromRequest(req) 209 | if err != nil { 210 | t.Errorf("error got %s", err.Error()) 211 | t.Fail() 212 | } else { 213 | if preq.OrderBy != "EMAIL" { 214 | t.Errorf("expect order by EMAIL but %s", preq.OrderBy) 215 | t.Fail() 216 | } 217 | if preq.Sort != "DESC" { 218 | t.Errorf("expect sort DESC but %s", preq.Sort) 219 | t.Fail() 220 | } 221 | if preq.No != 2 { 222 | t.Errorf("expect no 2 but %d", preq.No) 223 | t.Fail() 224 | } 225 | if preq.PageSize != 20 { 226 | t.Errorf("expect page size 20 but %d", preq.PageSize) 227 | t.Fail() 228 | } 229 | } 230 | } 231 | 232 | func TestNewPage(t *testing.T) { 233 | for idx, testPage := range pages { 234 | pr := &PageRequest{ 235 | No: testPage.No, 236 | PageSize: testPage.PageSize, 237 | OrderBy: testPage.OrderBy, 238 | Sort: testPage.Sort, 239 | } 240 | presult := NewPage(pr, testPage.TotalItems) 241 | t.Log("Testing page ", idx) 242 | if presult.No != testPage.No { 243 | t.Error("Test", idx, "Expect No", testPage.No, "but", presult.No) 244 | t.Fail() 245 | } 246 | if presult.TotalPages != testPage.TotalPages { 247 | t.Error("Test", idx, "Expect TotalPages", testPage.TotalPages, "but", presult.TotalPages) 248 | t.Fail() 249 | } 250 | if presult.PageSize != testPage.PageSize { 251 | t.Error("Test", idx, "Expect PageSize", testPage.PageSize, "but", presult.PageSize) 252 | t.Fail() 253 | } 254 | if presult.Items != testPage.Items { 255 | t.Error("Test", idx, "Expect Items", testPage.Items, "but", presult.Items) 256 | t.Fail() 257 | } 258 | if presult.TotalItems != testPage.TotalItems { 259 | t.Error("Test", idx, "Expect TotalItems", testPage.TotalItems, "but", presult.TotalItems) 260 | t.Fail() 261 | } 262 | if presult.HasNext != testPage.HasNext { 263 | t.Error("Test", idx, "Expect HasNext", testPage.HasNext, "but", presult.HasNext) 264 | t.Fail() 265 | } 266 | if presult.HasPrev != testPage.HasPrev { 267 | t.Error("Test", idx, "Expect HasPrev", testPage.HasPrev, "but", presult.HasPrev) 268 | t.Fail() 269 | } 270 | if presult.FistPage != testPage.FistPage { 271 | t.Error("Test", idx, "Expect FistPage", testPage.FistPage, "but", presult.FistPage) 272 | t.Fail() 273 | } 274 | if presult.LastPage != testPage.LastPage { 275 | t.Error("Test", idx, "Expect LastPage", testPage.LastPage, "but", presult.LastPage) 276 | t.Fail() 277 | } 278 | if presult.PrevPage != testPage.PrevPage { 279 | t.Error("Test", idx, "Expect PrevPage", testPage.PrevPage, "but", presult.PrevPage) 280 | t.Fail() 281 | } 282 | if presult.NextPage != testPage.NextPage { 283 | t.Error("Test", idx, "Expect NextPage", testPage.NextPage, "but", presult.NextPage) 284 | t.Fail() 285 | } 286 | if presult.OrderBy != testPage.OrderBy { 287 | t.Error("Test", idx, "Expect OrderBy", testPage.OrderBy, "but", presult.OrderBy) 288 | t.Fail() 289 | } 290 | if presult.Sort != testPage.Sort { 291 | t.Error("Test", idx, "Expect Sort", testPage.Sort, "but", presult.Sort) 292 | t.Fail() 293 | } 294 | if presult.OffsetStart != testPage.OffsetStart { 295 | t.Error("Test", idx, "Expect OffsetStart", testPage.OffsetStart, "but", presult.OffsetStart) 296 | t.Fail() 297 | } 298 | if presult.OffsetEnd != testPage.OffsetEnd { 299 | t.Error("Test", idx, "Expect OffsetEnd", testPage.OffsetEnd, "but", presult.OffsetEnd) 300 | t.Fail() 301 | } 302 | if presult.IsLast != testPage.IsLast { 303 | t.Error("Test", idx, "Expect IsLast", testPage.IsLast, "but", presult.IsLast) 304 | t.Fail() 305 | } 306 | if presult.IsFirst != testPage.IsFirst { 307 | t.Error("Test", idx, "Expect IsFirst", testPage.IsFirst, "but", presult.IsFirst) 308 | t.Fail() 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /internal/server/Server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gorilla/mux" 7 | "github.com/hyperjumptech/hansip/internal/config" 8 | "github.com/hyperjumptech/hansip/internal/connector" 9 | "github.com/hyperjumptech/hansip/internal/endpoint" 10 | "github.com/hyperjumptech/hansip/internal/gzip" 11 | "github.com/hyperjumptech/hansip/internal/mailer" 12 | "github.com/hyperjumptech/hansip/pkg/helper" 13 | "github.com/hyperjumptech/jiffy" 14 | "github.com/rs/cors" 15 | log "github.com/sirupsen/logrus" 16 | "net/http" 17 | "os" 18 | "os/signal" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | var ( 24 | // Router instance of gorilla mux.Router 25 | Router *mux.Router 26 | 27 | // TokenFactory will handle token creation and validation 28 | TokenFactory helper.TokenFactory 29 | ) 30 | 31 | // GetJwtTokenFactory return an instance of JWT TokenFactory. 32 | func GetJwtTokenFactory() helper.TokenFactory { 33 | accessDuration, err := jiffy.DurationOf(config.Get("token.access.duration")) 34 | if err != nil { 35 | panic(err) 36 | } 37 | refreshDuration, err := jiffy.DurationOf(config.Get("token.refresh.duration")) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | tokenFactory := helper.NewTokenFactory( 43 | config.Get("token.crypt.key"), 44 | config.Get("token.crypt.method"), 45 | config.Get("token.issuer"), 46 | accessDuration, 47 | refreshDuration) 48 | 49 | return tokenFactory 50 | } 51 | 52 | // InitializeRouter initializes Gorilla Mux and all handler, including Database and Mailer connector 53 | func InitializeRouter() { 54 | log.Info("Initializing server") 55 | Router = mux.NewRouter() 56 | 57 | if config.GetBoolean("server.http.cors.enable") { 58 | log.Info("CORS handling is enabled") 59 | options := cors.Options{ 60 | AllowedOrigins: strings.Split(config.Get("server.http.cors.allow.origins"), ","), 61 | AllowedHeaders: strings.Split(config.Get("server.http.cors.allow.headers"), ","), 62 | AllowCredentials: config.GetBoolean("server.http.cors.allow.credential"), 63 | AllowedMethods: strings.Split(config.Get("server.http.cors.allow.method"), ","), 64 | ExposedHeaders: strings.Split(config.Get("server.http.cors.exposed.headers"), ","), 65 | OptionsPassthrough: config.GetBoolean("server.http.cors.optionpassthrough"), 66 | MaxAge: config.GetInt("server.http.cors.maxage"), 67 | } 68 | log.Infof(" AllowedOrigins : %s", strings.Join(options.AllowedOrigins, ",")) 69 | log.Infof(" AllowedHeaders : %s", strings.Join(options.AllowedHeaders, ",")) 70 | log.Infof(" AllowedMethods : %s", strings.Join(options.AllowedMethods, ",")) 71 | log.Infof(" ExposedHeaders : %s", strings.Join(options.ExposedHeaders, ",")) 72 | log.Infof(" AllowCredentials : %v", options.AllowCredentials) 73 | log.Infof(" OptionsPassthrough : %v", options.OptionsPassthrough) 74 | log.Infof(" MaxAge : %d", options.MaxAge) 75 | c := cors.New(options) 76 | Router.Use(c.Handler) 77 | Router.Use(endpoint.CorsMiddleware) 78 | gzipFilter := gzip.NewGzipEncoderFilter(true, 300) 79 | Router.Use(gzipFilter.DoFilter) 80 | } 81 | 82 | Router.Use(endpoint.ClientIPResolverMiddleware, endpoint.TransactionIDMiddleware, endpoint.JwtMiddleware) 83 | 84 | if config.Get("db.type") == "MYSQL" { 85 | log.Warnf("Using MYSQL") 86 | endpoint.UserRepo = connector.GetMySQLDBInstance() 87 | endpoint.GroupRepo = connector.GetMySQLDBInstance() 88 | endpoint.RoleRepo = connector.GetMySQLDBInstance() 89 | endpoint.UserGroupRepo = connector.GetMySQLDBInstance() 90 | endpoint.UserRoleRepo = connector.GetMySQLDBInstance() 91 | endpoint.GroupRoleRepo = connector.GetMySQLDBInstance() 92 | endpoint.TenantRepo = connector.GetMySQLDBInstance() 93 | endpoint.RevocationRepo = connector.GetMySQLDBInstance() 94 | } else if config.Get("db.type") == "SQLITE" { 95 | log.Warnf("Using SQLITE") 96 | endpoint.UserRepo = connector.GetSqliteDBInstance() 97 | endpoint.GroupRepo = connector.GetSqliteDBInstance() 98 | endpoint.RoleRepo = connector.GetSqliteDBInstance() 99 | endpoint.UserGroupRepo = connector.GetSqliteDBInstance() 100 | endpoint.UserRoleRepo = connector.GetSqliteDBInstance() 101 | endpoint.GroupRoleRepo = connector.GetSqliteDBInstance() 102 | endpoint.TenantRepo = connector.GetSqliteDBInstance() 103 | endpoint.RevocationRepo = connector.GetSqliteDBInstance() 104 | } else { 105 | panic(fmt.Sprintf("unknown database type %s. Correct your configuration 'db.type' or env-var 'AAA_DB_TYPE'. allowed values are INMEMORY or MYSQL", config.Get("db.type"))) 106 | } 107 | 108 | if config.Get("mailer.type") == "DUMMY" { 109 | endpoint.EmailSender = &connector.DummyMailSender{} 110 | } else if config.Get("mailer.type") == "SENDMAIL" { 111 | endpoint.EmailSender = &connector.SendMailSender{ 112 | Host: config.Get("mailer.sendmail.host"), 113 | Port: config.GetInt("mailer.sendmail.port"), 114 | User: config.Get("mailer.sendmail.user"), 115 | Password: config.Get("mailer.sendmail.password"), 116 | } 117 | } else if config.Get("mailer.type") == "SENDGRID" { 118 | endpoint.EmailSender = &connector.SendGridSender{ 119 | Token: config.Get("mailer.sendgrid.token"), 120 | } 121 | } else { 122 | panic(fmt.Sprintf("unknown mailer type %s. Correct your configuration 'mailer.type' or env-var 'AAA_MAILER_TYPE'. allowed values are DUMMY, SENDMAIL or SENDGRID", config.Get("mailer.type"))) 123 | } 124 | mailer.Sender = endpoint.EmailSender 125 | 126 | TokenFactory = GetJwtTokenFactory() 127 | endpoint.TokenFactory = TokenFactory 128 | endpoint.TokenFactory = TokenFactory 129 | endpoint.InitializeRouter(Router) 130 | Walk() 131 | } 132 | 133 | func configureLogging() { 134 | lLevel := config.Get("server.log.level") 135 | fmt.Println("Setting log level to ", lLevel) 136 | switch strings.ToUpper(lLevel) { 137 | default: 138 | fmt.Println("Unknown level [", lLevel, "]. Log level set to ERROR") 139 | log.SetLevel(log.ErrorLevel) 140 | case "TRACE": 141 | log.SetLevel(log.TraceLevel) 142 | case "DEBUG": 143 | log.SetLevel(log.DebugLevel) 144 | case "INFO": 145 | log.SetLevel(log.InfoLevel) 146 | case "WARN": 147 | log.SetLevel(log.WarnLevel) 148 | case "ERROR": 149 | log.SetLevel(log.ErrorLevel) 150 | case "FATAL": 151 | log.SetLevel(log.FatalLevel) 152 | } 153 | } 154 | 155 | // Start this server 156 | func Start() { 157 | configureLogging() 158 | log.Infof("Starting Hansip") 159 | startTime := time.Now() 160 | 161 | InitializeRouter() 162 | go mailer.Start() 163 | 164 | var wait time.Duration 165 | 166 | graceShut, err := jiffy.DurationOf(config.Get("server.timeout.graceshut")) 167 | if err != nil { 168 | panic(err) 169 | } 170 | wait = graceShut 171 | WriteTimeout, err := jiffy.DurationOf(config.Get("server.timeout.write")) 172 | if err != nil { 173 | panic(err) 174 | } 175 | ReadTimeout, err := jiffy.DurationOf(config.Get("server.timeout.read")) 176 | if err != nil { 177 | panic(err) 178 | } 179 | IdleTimeout, err := jiffy.DurationOf(config.Get("server.timeout.idle")) 180 | if err != nil { 181 | panic(err) 182 | } 183 | 184 | address := fmt.Sprintf("%s:%s", config.Get("server.host"), config.Get("server.port")) 185 | log.Info("Server binding to ", address) 186 | 187 | srv := &http.Server{ 188 | Addr: address, 189 | // Good practice to set timeouts to avoid Slowloris attacks. 190 | WriteTimeout: WriteTimeout, 191 | ReadTimeout: ReadTimeout, 192 | IdleTimeout: IdleTimeout, 193 | Handler: Router, // Pass our instance of gorilla/mux in. 194 | } 195 | // Run our server in a goroutine so that it doesn't block. 196 | go func() { 197 | if err := srv.ListenAndServe(); err != nil { 198 | log.Println(err) 199 | } 200 | }() 201 | 202 | c := make(chan os.Signal, 1) 203 | // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) 204 | // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. 205 | signal.Notify(c, os.Interrupt) 206 | 207 | // Block until we receive our signal. 208 | <-c 209 | 210 | mailer.Stop() 211 | 212 | // Create a deadline to wait for. 213 | ctx, cancel := context.WithTimeout(context.Background(), wait) 214 | defer cancel() 215 | // Doesn't block if no connections, but will otherwise wait 216 | // until the timeout deadline. 217 | srv.Shutdown(ctx) 218 | // Optionally, you could run srv.Shutdown in a goroutine and block on 219 | // <-ctx.Done() if your application should wait for other services 220 | // to finalize based on context cancellation. 221 | dur := time.Now().Sub(startTime) 222 | durDesc := jiffy.DescribeDuration(dur, jiffy.NewWant()) 223 | log.Infof("Shutting down. This Hansip been protecting the world for %s", durDesc) 224 | os.Exit(0) 225 | } 226 | 227 | // Walk and show all endpoint that available on this server 228 | func Walk() { 229 | err := Router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 230 | pathTemplate, err := route.GetPathTemplate() 231 | if err != nil { 232 | log.Error(err) 233 | } 234 | methods, err := route.GetMethods() 235 | if err != nil { 236 | log.Error(err) 237 | } 238 | 239 | log.Infof("Route : %s [%s]", pathTemplate, strings.Join(methods, ",")) 240 | return nil 241 | }) 242 | 243 | if err != nil { 244 | log.Error(err) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /internal/endpoint/TenantManagement.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/hyperjumptech/hansip/internal/config" 11 | "github.com/hyperjumptech/hansip/internal/constants" 12 | "github.com/hyperjumptech/hansip/internal/hansipcontext" 13 | "github.com/hyperjumptech/hansip/pkg/helper" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var ( 18 | tenantMgmtLog = log.WithField("go", "TenantManagement") 19 | ) 20 | 21 | // ListAllTenants serving the listing of group tenants 22 | func ListAllTenants(w http.ResponseWriter, r *http.Request) { 23 | fLog := tenantMgmtLog.WithField("func", "ListAllTenans").WithField("RequestID", r.Context().Value(constants.RequestID)).WithField("path", r.URL.Path).WithField("method", r.Method) 24 | iauthctx := r.Context().Value(constants.HansipAuthentication) 25 | if iauthctx == nil { 26 | fLog.Tracef("Missing authentication context") 27 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "You are not authorized to access this resource", nil, nil) 28 | return 29 | } 30 | authCtx := iauthctx.(*hansipcontext.AuthenticationContext) 31 | if !authCtx.IsAnAdmin() { 32 | fLog.Tracef("Missing right") 33 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "You don't have the right to access this resource", nil, nil) 34 | return 35 | } 36 | pageRequest, err := helper.NewPageRequestFromRequest(r) 37 | if err != nil { 38 | fLog.Errorf("helper.NewPageRequestFromRequest got %s", err.Error()) 39 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 40 | return 41 | } 42 | tenants, page, err := TenantRepo.ListTenant(r.Context(), pageRequest) 43 | if err != nil { 44 | fLog.Errorf("TenantRepo.ListTenant got %s", err.Error()) 45 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 46 | return 47 | } 48 | ret := make(map[string]interface{}) 49 | ret["tenants"] = tenants 50 | ret["page"] = page 51 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "List of all tenants paginated", nil, ret) 52 | } 53 | 54 | // CreateTenantRequest hold model for Create new tenants 55 | type CreateTenantRequest struct { 56 | TenantName string `json:"name"` 57 | TenantDomain string `json:"domain"` 58 | Description string `json:"description"` 59 | } 60 | 61 | // CreateNewTenant serving request to create new tenant 62 | func CreateNewTenant(w http.ResponseWriter, r *http.Request) { 63 | fLog := tenantMgmtLog.WithField("func", "CreateNewTenant").WithField("RequestID", r.Context().Value(constants.RequestID)).WithField("path", r.URL.Path).WithField("method", r.Method) 64 | iauthctx := r.Context().Value(constants.HansipAuthentication) 65 | if iauthctx == nil { 66 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "You are not authorized to access this resource", nil, nil) 67 | return 68 | } 69 | authCtx := iauthctx.(*hansipcontext.AuthenticationContext) 70 | if !authCtx.IsAdminOfDomain(config.Get("hansip.domain")) { 71 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "You don't have the right to access this resource", nil, nil) 72 | return 73 | } 74 | 75 | req := &CreateTenantRequest{} 76 | body, err := ioutil.ReadAll(r.Body) 77 | if err != nil { 78 | fLog.Errorf("ioutil.ReadAll got %s", err.Error()) 79 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 80 | return 81 | } 82 | err = json.Unmarshal(body, req) 83 | if err != nil { 84 | fLog.Errorf("json.Unmarshal got %s", err.Error()) 85 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 86 | return 87 | } 88 | if strings.Contains(req.TenantDomain, "@") { 89 | fLog.Errorf("Domain contains @") 90 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, "Tenant domain contains @", nil, nil) 91 | return 92 | } 93 | tenant, err := TenantRepo.CreateTenantRecord(r.Context(), req.TenantName, req.TenantDomain, req.Description) 94 | if err != nil { 95 | fLog.Errorf("TenantRepo.CreateTenantRecord got %s", err.Error()) 96 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 97 | return 98 | } 99 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Success creating tenant", nil, tenant) 100 | return 101 | } 102 | 103 | // GetTenantDetail serving request to fetch tenant detail 104 | func GetTenantDetail(w http.ResponseWriter, r *http.Request) { 105 | fLog := tenantMgmtLog.WithField("func", "GetTenantDetail").WithField("RequestID", r.Context().Value(constants.RequestID)).WithField("path", r.URL.Path).WithField("method", r.Method) 106 | iauthctx := r.Context().Value(constants.HansipAuthentication) 107 | if iauthctx == nil { 108 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "You are not authorized to access this resource", nil, nil) 109 | return 110 | } 111 | authCtx := iauthctx.(*hansipcontext.AuthenticationContext) 112 | if !authCtx.IsAnAdmin() { 113 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "You don't have the right to access this resource", nil, nil) 114 | return 115 | } 116 | 117 | params, err := helper.ParsePathParams(fmt.Sprintf("%s/management/tenant/{tenantRecId}", apiPrefix), r.URL.Path) 118 | if err != nil { 119 | panic(err) 120 | } 121 | tenant, err := TenantRepo.GetTenantByRecID(r.Context(), params["tenantRecId"]) 122 | if err != nil { 123 | fLog.Errorf("TenantRepo.GetTenantByRecID got %s", err.Error()) 124 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 125 | return 126 | } 127 | if tenant == nil { 128 | helper.WriteHTTPResponse(r.Context(), w, http.StatusNotFound, fmt.Sprintf("Tenant recid %s not exist", params["tenantRecId"]), nil, nil) 129 | return 130 | } 131 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Tenant retrieved", nil, tenant) 132 | } 133 | 134 | // UpdateTenantDetail serving request to update tenant detail 135 | func UpdateTenantDetail(w http.ResponseWriter, r *http.Request) { 136 | fLog := tenantMgmtLog.WithField("func", "UpdateTenantDetail").WithField("RequestID", r.Context().Value(constants.RequestID)).WithField("path", r.URL.Path).WithField("method", r.Method) 137 | iauthctx := r.Context().Value(constants.HansipAuthentication) 138 | if iauthctx == nil { 139 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "You are not authorized to access this resource", nil, nil) 140 | return 141 | } 142 | authCtx := iauthctx.(*hansipcontext.AuthenticationContext) 143 | if !authCtx.IsAdminOfDomain(config.Get("hansip.domain")) { 144 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "You don't have the right to access this resource", nil, nil) 145 | return 146 | } 147 | 148 | params, err := helper.ParsePathParams(fmt.Sprintf("%s/management/tenant/{tenantRecId}", apiPrefix), r.URL.Path) 149 | if err != nil { 150 | panic(err) 151 | } 152 | 153 | req := &CreateTenantRequest{} 154 | body, err := ioutil.ReadAll(r.Body) 155 | if err != nil { 156 | fLog.Errorf("ioutil.ReadAll got %s", err.Error()) 157 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 158 | return 159 | } 160 | err = json.Unmarshal(body, req) 161 | if err != nil { 162 | fLog.Errorf("json.Unmarshal got %s", err.Error()) 163 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 164 | return 165 | } 166 | 167 | tenant, err := TenantRepo.GetTenantByRecID(r.Context(), params["tenantRecId"]) 168 | if err != nil { 169 | fLog.Errorf("TenantRepo.GetTenantByRecID got %s", err.Error()) 170 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 171 | return 172 | } 173 | if tenant == nil { 174 | helper.WriteHTTPResponse(r.Context(), w, http.StatusNotFound, fmt.Sprintf("Tenant recid %s not exist", params["tenantRecId"]), nil, nil) 175 | return 176 | } 177 | tenant.Name = req.TenantName 178 | tenant.Domain = req.TenantDomain 179 | tenant.Description = req.Description 180 | 181 | exTenant, err := TenantRepo.GetTenantByDomain(r.Context(), req.TenantDomain) 182 | if err == nil && exTenant.Domain == req.TenantDomain && exTenant.RecID != params["tenantRecId"] { 183 | fLog.Errorf("Duplicate tenant domain name. tenant domain %s already exist", req.TenantDomain) 184 | helper.WriteHTTPResponse(r.Context(), w, http.StatusNotFound, err.Error(), nil, nil) 185 | return 186 | } 187 | 188 | err = TenantRepo.UpdateTenant(r.Context(), tenant) 189 | if err != nil { 190 | fLog.Errorf("TenantRepo.UpdateTenant got %s", err.Error()) 191 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 192 | return 193 | } 194 | 195 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Tenant updated", nil, tenant) 196 | 197 | } 198 | 199 | // DeleteTenant serving request to delete a tenant 200 | func DeleteTenant(w http.ResponseWriter, r *http.Request) { 201 | fLog := tenantMgmtLog.WithField("func", "DeleteTenant").WithField("RequestID", r.Context().Value(constants.RequestID)).WithField("path", r.URL.Path).WithField("method", r.Method) 202 | iauthctx := r.Context().Value(constants.HansipAuthentication) 203 | if iauthctx == nil { 204 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "You are not authorized to access this resource", nil, nil) 205 | return 206 | } 207 | authCtx := iauthctx.(*hansipcontext.AuthenticationContext) 208 | if !authCtx.IsAdminOfDomain(config.Get("hansip.domain")) { 209 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "You don't have the right to access this resource", nil, nil) 210 | return 211 | } 212 | 213 | params, err := helper.ParsePathParams(fmt.Sprintf("%s/management/tenant/{tenantRecId}", apiPrefix), r.URL.Path) 214 | if err != nil { 215 | panic(err) 216 | } 217 | tenant, err := TenantRepo.GetTenantByRecID(r.Context(), params["tenantRecId"]) 218 | if err != nil { 219 | fLog.Errorf("TenantRepo.GetTenantByRecID got %s", err.Error()) 220 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 221 | return 222 | } 223 | if tenant == nil { 224 | helper.WriteHTTPResponse(r.Context(), w, http.StatusNotFound, fmt.Sprintf("Tenant recid %s not exist", params["tenantRecId"]), nil, nil) 225 | return 226 | } 227 | err = TenantRepo.DeleteTenant(r.Context(), tenant) 228 | if err != nil { 229 | fLog.Errorf("TenantRepo.DeleteTenant got %s", err.Error()) 230 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 231 | return 232 | } 233 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Group deleted", nil, nil) 234 | } 235 | -------------------------------------------------------------------------------- /internal/endpoint/Management.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gorilla/mux" 7 | "github.com/hyperjumptech/hansip/api" 8 | "github.com/hyperjumptech/hansip/internal/config" 9 | "github.com/hyperjumptech/hansip/internal/connector" 10 | ) 11 | 12 | var ( 13 | // TenantRepo is a user Repository instance 14 | TenantRepo connector.TenantRepository 15 | // UserRepo is a user Repository instance 16 | UserRepo connector.UserRepository 17 | // GroupRepo is a group repository instance 18 | GroupRepo connector.GroupRepository 19 | // RoleRepo is a role repository instance 20 | RoleRepo connector.RoleRepository 21 | // UserGroupRepo is a user group repository instance 22 | UserGroupRepo connector.UserGroupRepository 23 | // UserRoleRepo is a user role repository instance 24 | UserRoleRepo connector.UserRoleRepository 25 | // GroupRoleRepo is a group role repository instance 26 | GroupRoleRepo connector.GroupRoleRepository 27 | // RevocationRepo is a revocation repository instance 28 | RevocationRepo connector.RevocationRepository 29 | // EmailSender is email sender instance 30 | EmailSender connector.EmailSender 31 | 32 | apiPrefix = config.Get("api.path.prefix") 33 | // Endpoints slice of endpoint pointers 34 | Endpoints []*Endpoint 35 | ) 36 | 37 | func init() { 38 | 39 | if len(apiPrefix) == 0 { 40 | panic("API prefix is not configured. please configure variable 'api.path.prefix' or AAA_API_PATH_PREFIX env variable") 41 | } 42 | 43 | hansipAdmin := fmt.Sprintf("%s@%s", config.Get("hansip.admin"), config.Get("hansip.domain")) 44 | anyUser := "*@*" 45 | adminUser := fmt.Sprintf("%s@*", config.Get("hansip.admin")) 46 | 47 | Endpoints = []*Endpoint{ 48 | {"/docs/**/*", GetMethod, true, nil, api.ServeStatic}, 49 | {"/health", GetMethod, true, nil, HealthCheck}, 50 | {fmt.Sprintf("%s/auth/authenticate", apiPrefix), OptionMethod | PostMethod, true, nil, Authentication}, 51 | {fmt.Sprintf("%s/auth/refresh", apiPrefix), OptionMethod | PostMethod, false, []string{anyUser}, Refresh}, 52 | {fmt.Sprintf("%s/auth/2fa", apiPrefix), OptionMethod | PostMethod, true, nil, TwoFA}, 53 | {fmt.Sprintf("%s/auth/2fatest", apiPrefix), OptionMethod | PostMethod, false, []string{anyUser}, TwoFATest}, 54 | {fmt.Sprintf("%s/auth/authenticate2fa", apiPrefix), OptionMethod | PostMethod, false, nil, Authentication2FA}, 55 | 56 | {fmt.Sprintf("%s/management/tenants", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListAllTenants}, 57 | {fmt.Sprintf("%s/management/tenant", apiPrefix), OptionMethod | PostMethod, false, []string{hansipAdmin}, CreateNewTenant}, 58 | {fmt.Sprintf("%s/management/tenant/{tenantRecId}", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, GetTenantDetail}, 59 | {fmt.Sprintf("%s/management/tenant/{tenantRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{hansipAdmin}, UpdateTenantDetail}, 60 | {fmt.Sprintf("%s/management/tenant/{tenantRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{hansipAdmin}, DeleteTenant}, 61 | 62 | {fmt.Sprintf("%s/management/users", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListAllUsers}, 63 | {fmt.Sprintf("%s/management/user", apiPrefix), OptionMethod | PostMethod, false, []string{adminUser}, CreateNewUser}, 64 | {fmt.Sprintf("%s/management/user/{userRecId}/passwd", apiPrefix), OptionMethod | PostMethod, false, nil, ChangePassphrase}, 65 | {fmt.Sprintf("%s/management/user/activate", apiPrefix), OptionMethod | PostMethod, true, []string{adminUser}, ActivateUser}, 66 | {fmt.Sprintf("%s/management/user/whoami", apiPrefix), OptionMethod | GetMethod, false, []string{anyUser}, WhoAmI}, 67 | {fmt.Sprintf("%s/management/user/2FAQR", apiPrefix), OptionMethod | GetMethod, false, nil, Show2FAQrCode}, 68 | {fmt.Sprintf("%s/management/user/activate2FA", apiPrefix), OptionMethod | PostMethod, false, nil, Activate2FA}, 69 | {fmt.Sprintf("%s/management/user/{userRecId}", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, GetUserDetail}, 70 | {fmt.Sprintf("%s/management/user/{userRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, UpdateUserDetail}, 71 | {fmt.Sprintf("%s/management/user/{userRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteUser}, 72 | {fmt.Sprintf("%s/management/user/{userRecId}/roles", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListUserRole}, 73 | {fmt.Sprintf("%s/management/user/{userRecId}/roles", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, SetUserRoles}, 74 | {fmt.Sprintf("%s/management/user/{userRecId}/roles", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteUserRoles}, 75 | {fmt.Sprintf("%s/management/user/{userRecId}/all-roles", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListAllUserRole}, 76 | {fmt.Sprintf("%s/management/user/{userRecId}/role/{roleRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, CreateUserRole}, 77 | {fmt.Sprintf("%s/management/user/{userRecId}/role/{roleRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteUserRole}, 78 | {fmt.Sprintf("%s/management/user/{userRecId}/groups", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListUserGroup}, 79 | {fmt.Sprintf("%s/management/user/{userRecId}/groups", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, SetUserGroups}, 80 | {fmt.Sprintf("%s/management/user/{userRecId}/groups", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteUserGroups}, 81 | {fmt.Sprintf("%s/management/user/{userRecId}/group/{groupRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, CreateUserGroup}, 82 | {fmt.Sprintf("%s/management/user/{userRecId}/group/{groupRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteUserGroup}, 83 | 84 | {fmt.Sprintf("%s/management/tenant/{tenantRecId}/groups", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListAllGroup}, 85 | {fmt.Sprintf("%s/management/group", apiPrefix), OptionMethod | PostMethod, false, []string{adminUser}, CreateNewGroup}, 86 | {fmt.Sprintf("%s/management/group/{groupRecId}", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, GetGroupDetail}, 87 | {fmt.Sprintf("%s/management/group/{groupRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteGroup}, 88 | {fmt.Sprintf("%s/management/group/{groupRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, UpdateGroup}, 89 | {fmt.Sprintf("%s/management/group/{groupRecId}/users", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListGroupUser}, 90 | {fmt.Sprintf("%s/management/group/{groupRecId}/users", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, SetGroupUsers}, 91 | {fmt.Sprintf("%s/management/group/{groupRecId}/users", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteGroupUsers}, 92 | {fmt.Sprintf("%s/management/group/{groupRecId}/user/{userRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, CreateGroupUser}, 93 | {fmt.Sprintf("%s/management/group/{groupRecId}/user/{userRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteGroupUser}, 94 | {fmt.Sprintf("%s/management/group/{groupRecId}/roles", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListGroupRole}, 95 | {fmt.Sprintf("%s/management/group/{groupRecId}/roles", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, SetGroupRoles}, 96 | {fmt.Sprintf("%s/management/group/{groupRecId}/roles", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteGroupRoles}, 97 | {fmt.Sprintf("%s/management/group/{groupRecId}/role/{roleRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, CreateGroupRole}, 98 | {fmt.Sprintf("%s/management/group/{groupRecId}/role/{roleRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteGroupRole}, 99 | 100 | {fmt.Sprintf("%s/management/tenant/{tenantRecId}/roles", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListAllRole}, 101 | {fmt.Sprintf("%s/management/role", apiPrefix), OptionMethod | PostMethod, false, []string{adminUser}, CreateRole}, 102 | {fmt.Sprintf("%s/management/role/{roleRecId}", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, GetRoleDetail}, 103 | {fmt.Sprintf("%s/management/role/{roleRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteRole}, 104 | {fmt.Sprintf("%s/management/role/{roleRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, UpdateRole}, 105 | {fmt.Sprintf("%s/management/role/{roleRecId}/users", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListRoleUser}, 106 | {fmt.Sprintf("%s/management/role/{roleRecId}/users", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, SetRoleUsers}, 107 | {fmt.Sprintf("%s/management/role/{roleRecId}/users", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteRoleUsers}, 108 | {fmt.Sprintf("%s/management/role/{roleRecId}/user/{userRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, CreateRoleUser}, 109 | {fmt.Sprintf("%s/management/role/{roleRecId}/user/{userRecId}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteRoleUser}, 110 | {fmt.Sprintf("%s/management/role/{roleRecId}/groups", apiPrefix), OptionMethod | GetMethod, false, []string{adminUser}, ListRoleGroup}, 111 | {fmt.Sprintf("%s/management/role/{roleRecId}/groups", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, SetRoleGroups}, 112 | {fmt.Sprintf("%s/management/role/{roleRecId}/groups", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteRoleGroups}, 113 | {fmt.Sprintf("%s/management/role/{roleRecId}/group/{groupRecId}", apiPrefix), OptionMethod | PutMethod, false, []string{adminUser}, CreateRoleGroup}, 114 | {fmt.Sprintf("%s/management/role/{roleRecId}/group/{GroupRecID}", apiPrefix), OptionMethod | DeleteMethod, false, []string{adminUser}, DeleteRoleGroup}, 115 | 116 | {fmt.Sprintf("%s/recovery/recoverPassphrase", apiPrefix), OptionMethod | PostMethod, true, nil, RecoverPassphrase}, 117 | {fmt.Sprintf("%s/recovery/resetPassphrase", apiPrefix), OptionMethod | PostMethod, true, nil, ResetPassphrase}, 118 | } 119 | } 120 | 121 | // InitializeRouter will initialize router to execute management endpoints 122 | func InitializeRouter(router *mux.Router) { 123 | for path := range api.StaticResources { 124 | router.HandleFunc(path, api.ServeStatic).Methods("GET") 125 | } 126 | for _, ep := range Endpoints { 127 | router.HandleFunc(ep.PathPattern, ep.HandleFunction).Methods(FlagToListMethod(ep.AllowedMethodFlag)...) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /internal/connector/DataAccessObjects.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "context" 5 | "github.com/hyperjumptech/hansip/pkg/helper" 6 | "time" 7 | ) 8 | 9 | // DBUtil is interface to help working with table create and drop 10 | type DBUtil interface { 11 | // DropAllTables will drop all existing table 12 | DropAllTables(ctx context.Context) error 13 | 14 | // CreateAllTable will create tables needed for the Apps if not exist 15 | CreateAllTable(ctx context.Context) error 16 | } 17 | 18 | // TenantRepository manage tenant table 19 | type TenantRepository interface { 20 | // GetTenantByDomain return a tenant record 21 | GetTenantByDomain(ctx context.Context, tenantDomain string) (*Tenant, error) 22 | 23 | // GetTenantByRecID return a tenant record 24 | GetTenantByRecID(ctx context.Context, recID string) (*Tenant, error) 25 | 26 | // CreateTenantRecord Create new tenant 27 | CreateTenantRecord(ctx context.Context, tenantName, tenantDomain, description string) (*Tenant, error) 28 | 29 | // DeleteTenant removes a tenant entity from table 30 | DeleteTenant(ctx context.Context, tenant *Tenant) error 31 | 32 | // SaveOrUpdate a tenant entity into table tenant 33 | UpdateTenant(ctx context.Context, tenant *Tenant) error 34 | 35 | // ListTenant from database with pagination 36 | ListTenant(ctx context.Context, request *helper.PageRequest) ([]*Tenant, *helper.Page, error) 37 | } 38 | 39 | // UserRepository manage User table 40 | type UserRepository interface { 41 | // GetUserByRecID return a user record 42 | GetUserByRecID(ctx context.Context, recID string) (*User, error) 43 | 44 | // CreateUserRecord in the User table 45 | CreateUserRecord(ctx context.Context, email, passphrase string) (*User, error) 46 | 47 | // GetUserByEmail return a user record 48 | GetUserByEmail(ctx context.Context, email string) (*User, error) 49 | 50 | // GetUserBy2FAToken return a user record 51 | GetUserBy2FAToken(ctx context.Context, token string) (*User, error) 52 | 53 | // GetUserByRecoveryToken return user record 54 | GetUserByRecoveryToken(ctx context.Context, token string) (*User, error) 55 | 56 | // DeleteUser removes a user entity from table 57 | DeleteUser(ctx context.Context, user *User) error 58 | 59 | // SaveOrUpdate a user entity into table user 60 | UpdateUser(ctx context.Context, user *User) error 61 | 62 | // ListUser from database with pagination 63 | ListUser(ctx context.Context, request *helper.PageRequest) ([]*User, *helper.Page, error) 64 | 65 | // Count all user entity in table 66 | Count(ctx context.Context) (int, error) 67 | 68 | // ListAllUserRoles will list all roles owned by a particular user 69 | ListAllUserRoles(ctx context.Context, user *User, request *helper.PageRequest) ([]*Role, *helper.Page, error) 70 | 71 | // GetTOTPRecoveryCodes retrieves all valid/not used TOTP recovery codes. 72 | GetTOTPRecoveryCodes(ctx context.Context, user *User) ([]string, error) 73 | 74 | // RecreateTOTPRecoveryCodes recreates 16 new recovery codes. 75 | RecreateTOTPRecoveryCodes(ctx context.Context, user *User) ([]string, error) 76 | 77 | // MarkTOTPRecoveryCodeUsed will mark the specific recovery code as used and thus can not be used anymore. 78 | MarkTOTPRecoveryCodeUsed(ctx context.Context, user *User, code string) error 79 | } 80 | 81 | // GroupRepository manage Group table 82 | type GroupRepository interface { 83 | // GetGroupByRecID return a group record 84 | GetGroupByRecID(ctx context.Context, recID string) (*Group, error) 85 | 86 | // GetGroupByName return a group record 87 | GetGroupByName(ctx context.Context, groupName, groupDomain string) (*Group, error) 88 | 89 | // CreateGroup into the Group table 90 | CreateGroup(ctx context.Context, groupName, groupDomain, description string) (*Group, error) 91 | 92 | // ListGroup from the Group table 93 | ListGroups(ctx context.Context, tenant *Tenant, request *helper.PageRequest) ([]*Group, *helper.Page, error) 94 | 95 | // DeleteGroup from Group table 96 | DeleteGroup(ctx context.Context, group *Group) error 97 | 98 | // CreateUserGroup into Group table 99 | UpdateGroup(ctx context.Context, group *Group) error 100 | } 101 | 102 | // UserGroupRepository manage UserGroup table 103 | type UserGroupRepository interface { 104 | // GetUserGroup returns existing UserGroup 105 | GetUserGroup(ctx context.Context, user *User, group *Group) (*UserGroup, error) 106 | 107 | // CreateUserGroup into UserGroup table 108 | CreateUserGroup(ctx context.Context, user *User, group *Group) (*UserGroup, error) 109 | 110 | // ListUserGroupByEmail from the UserGroup table 111 | ListUserGroupByUser(ctx context.Context, user *User, request *helper.PageRequest) ([]*Group, *helper.Page, error) 112 | 113 | // ListUserGroupByGroupName from the UserGroup table 114 | ListUserGroupByGroup(ctx context.Context, group *Group, request *helper.PageRequest) ([]*User, *helper.Page, error) 115 | 116 | // DeleteUserGroup from the UserGroup table 117 | DeleteUserGroup(ctx context.Context, userGroup *UserGroup) error 118 | 119 | // DeleteUserGroupByEmail from the UserGroup table 120 | DeleteUserGroupByUser(ctx context.Context, user *User) error 121 | 122 | // DeleteUserGroupByGroupName from the UserGroup table 123 | DeleteUserGroupByGroup(ctx context.Context, group *Group) error 124 | } 125 | 126 | // UserRoleRepository manage UserRole table 127 | type UserRoleRepository interface { 128 | // GetUserRole returns existing user role 129 | GetUserRole(ctx context.Context, user *User, role *Role) (*UserRole, error) 130 | 131 | // CreateUserRole into UserRole table 132 | CreateUserRole(ctx context.Context, user *User, role *Role) (*UserRole, error) 133 | 134 | // ListUserRoleByEmail from UserRole table 135 | ListUserRoleByUser(ctx context.Context, user *User, request *helper.PageRequest) ([]*Role, *helper.Page, error) 136 | 137 | // ListUserRoleByRoleName from UserRole table 138 | ListUserRoleByRole(ctx context.Context, role *Role, request *helper.PageRequest) ([]*User, *helper.Page, error) 139 | 140 | // DeleteUserRole from UserRole table 141 | DeleteUserRole(ctx context.Context, userRole *UserRole) error 142 | 143 | // DeleteUserRoleByEmail from UserRole table 144 | DeleteUserRoleByUser(ctx context.Context, user *User) error 145 | 146 | // DeleteUserRoleByRoleName from UserRole table 147 | DeleteUserRoleByRole(ctx context.Context, role *Role) error 148 | } 149 | 150 | // GroupRoleRepository manage GroupRole table 151 | type GroupRoleRepository interface { 152 | 153 | // GetGroupRole return existing group role 154 | GetGroupRole(ctx context.Context, group *Group, role *Role) (*GroupRole, error) 155 | 156 | // CreateGroupRole into GroupRole table 157 | CreateGroupRole(ctx context.Context, group *Group, role *Role) (*GroupRole, error) 158 | 159 | // ListGroupRoleByGroupName from GroupRole table 160 | ListGroupRoleByGroup(ctx context.Context, group *Group, request *helper.PageRequest) ([]*Role, *helper.Page, error) 161 | 162 | // ListGroupRoleByRoleName from GroupRole table 163 | ListGroupRoleByRole(ctx context.Context, role *Role, request *helper.PageRequest) ([]*Group, *helper.Page, error) 164 | 165 | // DeleteGroupRole from GroupRole table 166 | DeleteGroupRole(ctx context.Context, groupRole *GroupRole) error 167 | 168 | // DeleteGroupRoleByEmail from GroupRole table 169 | DeleteGroupRoleByGroup(ctx context.Context, group *Group) error 170 | 171 | // DeleteGroupRoleByRoleName from GroupRole table 172 | DeleteGroupRoleByRole(ctx context.Context, role *Role) error 173 | } 174 | 175 | // RoleRepository manage Role table 176 | type RoleRepository interface { 177 | // GetRoleByRecID return an existing role 178 | GetRoleByRecID(ctx context.Context, recID string) (*Role, error) 179 | 180 | // GetRoleByName return a role record 181 | GetRoleByName(ctx context.Context, roleName, roleDomain string) (*Role, error) 182 | 183 | // CreateRole into Role table 184 | CreateRole(ctx context.Context, roleName, roleDomain, description string) (*Role, error) 185 | 186 | // ListRoles from Role table 187 | ListRoles(ctx context.Context, tenant *Tenant, request *helper.PageRequest) ([]*Role, *helper.Page, error) 188 | 189 | // DeleteRole from Role table 190 | DeleteRole(ctx context.Context, role *Role) error 191 | 192 | // SaveOrUpdateRole into Role table 193 | UpdateRole(ctx context.Context, role *Role) error 194 | } 195 | 196 | // RevocationRepository manage revocation table 197 | type RevocationRepository interface { 198 | // Revoke a subject 199 | Revoke(ctx context.Context, subject string) error 200 | 201 | // UnRevoke a subject 202 | UnRevoke(ctx context.Context, subject string) error 203 | 204 | // IsRevoked validate if a subject is revoked 205 | IsRevoked(ctx context.Context, subject string) (bool, error) 206 | } 207 | 208 | // Revocation record entity 209 | type Revocation struct { 210 | // TenantName is the tenant name 211 | Subject string `json:"subject"` 212 | 213 | // RevocationTime is the time of revocation 214 | RevocationTime time.Time `json:"revocation_time"` 215 | } 216 | 217 | // Tenant record entity 218 | type Tenant struct { 219 | // RecID. Primary key 220 | RecID string `json:"rec_id"` 221 | 222 | // TenantName is the tenant name 223 | Name string `json:"name"` 224 | 225 | // Description of the group 226 | Description string `json:"description"` 227 | 228 | // TenantAdminRole role needed to manage users under this tenant 229 | Domain string `json:"domain"` 230 | } 231 | 232 | // User record entity 233 | type User struct { 234 | // RecID. Primary key 235 | RecID string `json:"rec_id"` 236 | 237 | // Email address. unique 238 | Email string `json:"email"` 239 | 240 | // HashedPassphrase bcrypt hashed passphrase 241 | HashedPassphrase string `json:"hashed_passphrase"` 242 | 243 | // Enabled status of the user 244 | Enabled bool `json:"enabled"` 245 | 246 | // Suspended status of the user 247 | Suspended bool `json:"suspended"` 248 | 249 | // LastSeen time of the user 250 | LastSeen time.Time `json:"last_seen"` 251 | 252 | // LastLogin time of the user 253 | LastLogin time.Time `json:"last_login"` 254 | 255 | // FailCount of login attempt 256 | FailCount int `json:"fail_count"` 257 | 258 | // ActivationCode for activating/enabling the user 259 | ActivationCode string `json:"activation_code"` 260 | 261 | // ActivationDate time of the user 262 | ActivationDate time.Time `json:"activation_date"` 263 | 264 | // UserTotpSecretKey for 2 factor authentication 265 | UserTotpSecretKey string `json:"user_totp_secret_key"` 266 | 267 | // Enable2FactorAuth used for enabling 2 factor auth 268 | Enable2FactorAuth bool `json:"enable_2_factor_auth"` 269 | 270 | // Token2FA used to authenticate back using 2FA 271 | Token2FA string `json:"token_2_fa"` 272 | 273 | // RecoveryCode used to recover lost passphrase 274 | RecoveryCode string `json:"recovery_code"` 275 | 276 | // The tenant owner 277 | TenantRecId string `json:"tenant_rec_id"` 278 | } 279 | 280 | // TOTPRecoveryCode used to login the user if the user lost his TOTP code due to lost of 2FE token device. 281 | type TOTPRecoveryCode struct { 282 | // RecID. Primary Key 283 | RecID string `json:"rec_id"` 284 | 285 | // The 8 digit key used once code. No dash separator. Only upper A-Z and 0-9 286 | Code string `json:"code"` 287 | 288 | // The used flag. If true, this token can not be used anymore. 289 | Used bool `json:"used"` 290 | 291 | // The owner of this code. 292 | UserRecID string `json:"user_rec_id"` 293 | } 294 | 295 | // Group record entity 296 | type Group struct { 297 | // RecID. Primary key 298 | RecID string `json:"rec_id"` 299 | 300 | // GroupName of the group, Primary Key 301 | GroupName string `json:"group_name"` 302 | 303 | // GroupDomain domain of the group, Primary Key 304 | GroupDomain string `json:"group_domain"` 305 | 306 | // Description of the group 307 | Description string `json:"description"` 308 | 309 | // The tenant owner 310 | TenantRecId string `json:"tenant_rec_id"` 311 | } 312 | 313 | // UserGroup record entity 314 | type UserGroup struct { 315 | // Email composite key to User 316 | UserRecID string `json:"user_rec_id"` 317 | 318 | // GroupName composite key to Group 319 | GroupRecID string `json:"group_rec_id"` 320 | } 321 | 322 | // UserRole record entity 323 | type UserRole struct { 324 | // Email composite key to User 325 | UserRecID string `json:"user_rec_id"` 326 | 327 | // RoleName composite key to Role 328 | RoleRecID string `json:"role_rec_id"` 329 | } 330 | 331 | // GroupRole record entity 332 | type GroupRole struct { 333 | // GroupName composite key to Group 334 | GroupRecID string `json:"group_rec_id"` 335 | 336 | // RoleName composite key to Role 337 | RoleRecID string `json:"role_rec_id"` 338 | } 339 | 340 | // Role record entity 341 | type Role struct { 342 | // RecID. Primary key 343 | RecID string `json:"rec_id"` 344 | 345 | // RoleName of the role, Unique 346 | RoleName string `json:"role_name"` 347 | 348 | // RoleDomain domain of the role, Unique 349 | RoleDomain string `json:"role_domain"` 350 | 351 | // Description of the role 352 | Description string `json:"description"` 353 | 354 | // The tenant owner 355 | TenantRecId string `json:"tenant_rec_id"` 356 | } 357 | -------------------------------------------------------------------------------- /internal/endpoint/Authentication.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hyperjumptech/hansip/pkg/helper" 7 | "github.com/hyperjumptech/hansip/pkg/totp" 8 | "golang.org/x/crypto/bcrypt" 9 | "io/ioutil" 10 | "net/http" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | var ( 16 | 17 | // TokenFactory instance used for generating and validating token 18 | TokenFactory helper.TokenFactory 19 | 20 | initialized = false 21 | ) 22 | 23 | // Request a model for authentication request. 24 | type Request struct { 25 | Email string `json:"email"` 26 | Passphrase string `json:"passphrase"` 27 | } 28 | 29 | // RequestWith2FA a model for authentication using 2fa secret key 30 | type RequestWith2FA struct { 31 | Email string `json:"email"` 32 | Passphrase string `json:"passphrase"` 33 | SecretKey string `json:"2FA_recovery_code"` 34 | } 35 | 36 | // Response a model for responding successful authentication 37 | type Response struct { 38 | AccessToken string `json:"access_token"` 39 | RefreshToken string `json:"refresh_token"` 40 | } 41 | 42 | // RefreshResponse a model for responding successful refresh 43 | type RefreshResponse struct { 44 | AccessToken string `json:"access_token"` 45 | } 46 | 47 | // TwoFARequest model for sending 2FA authentication 48 | type TwoFARequest struct { 49 | Token string `json:"2FA_token"` 50 | Otp string `json:"2FA_otp"` 51 | } 52 | 53 | // TwoFATestRequest model for sending 2FA authentication 54 | type TwoFATestRequest struct { 55 | Email string `json:"email"` 56 | Otp string `json:"2FA_otp"` 57 | } 58 | 59 | // TwoFATest is an endpoint handler used for testing 2FA 60 | func TwoFATest(w http.ResponseWriter, r *http.Request) { 61 | body, err := ioutil.ReadAll(r.Body) 62 | if err != nil { 63 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 64 | return 65 | } 66 | authReq := &TwoFATestRequest{} 67 | err = json.Unmarshal(body, authReq) 68 | if err != nil { 69 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 70 | return 71 | } 72 | user, err := UserRepo.GetUserByEmail(r.Context(), authReq.Email) 73 | if err != nil { 74 | helper.WriteHTTPResponse(r.Context(), w, http.StatusNotFound, err.Error(), nil, nil) 75 | return 76 | } 77 | 78 | secret := totp.SecretFromBase32(user.UserTotpSecretKey) 79 | valid, err := totp.Authenticate(secret, authReq.Otp, true) 80 | if err != nil { 81 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 82 | return 83 | } 84 | if !valid { 85 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "OTP not valid", nil, nil) 86 | } else { 87 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "OTP Valid", nil, nil) 88 | } 89 | return 90 | } 91 | 92 | // TwoFA validate 2FA token and authenticate the user 93 | func TwoFA(w http.ResponseWriter, r *http.Request) { 94 | body, err := ioutil.ReadAll(r.Body) 95 | if err != nil { 96 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 97 | return 98 | } 99 | authReq := &TwoFARequest{} 100 | err = json.Unmarshal(body, authReq) 101 | if err != nil { 102 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 103 | return 104 | } 105 | user, err := UserRepo.GetUserBy2FAToken(r.Context(), authReq.Token) 106 | if err != nil || user == nil { 107 | helper.WriteHTTPResponse(r.Context(), w, http.StatusNotFound, err.Error(), nil, nil) 108 | return 109 | } 110 | 111 | secret := totp.SecretFromBase32(user.UserTotpSecretKey) 112 | valid, err := totp.Authenticate(secret, authReq.Otp, true) 113 | if err != nil { 114 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 115 | return 116 | } 117 | 118 | defer UserRepo.UpdateUser(r.Context(), user) 119 | 120 | if !valid { 121 | user.FailCount = user.FailCount + 1 122 | if user.FailCount > 3 { 123 | user.Suspended = true 124 | } 125 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "OTP not valid", nil, nil) 126 | return 127 | } 128 | 129 | // If the password is valid, reset the user's FailCount 130 | user.FailCount = 0 131 | 132 | var roles []string 133 | 134 | // Add user's role from direct UserRole relation. 135 | userRoles, _, err := UserRepo.ListAllUserRoles(r.Context(), user, &helper.PageRequest{ 136 | No: 1, 137 | PageSize: 1000, 138 | OrderBy: "ROLE_NAME", 139 | Sort: "ASC", 140 | }) 141 | if err != nil { 142 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 143 | return 144 | } 145 | // Add user's role into Token audiences info. 146 | roles = make([]string, len(userRoles)) 147 | for k, v := range userRoles { 148 | r, err := RoleRepo.GetRoleByRecID(r.Context(), v.RecID) 149 | if err == nil { 150 | roles[k] = r.RoleName 151 | } 152 | } 153 | 154 | // Set the account email into Token subject. 155 | subject := user.Email 156 | 157 | // Set the audience 158 | audience := roles 159 | 160 | access, refresh, err := TokenFactory.CreateTokenPair(subject, audience, nil) 161 | 162 | resp := &Response{ 163 | AccessToken: access, 164 | RefreshToken: refresh, 165 | } 166 | 167 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Successful", nil, resp) 168 | } 169 | 170 | // Authentication2FA serve authentication with 2fa secret key 171 | func Authentication2FA(w http.ResponseWriter, r *http.Request) { 172 | // Check content-type, make sure its application/json 173 | cType := r.Header.Get("Content-Type") 174 | if cType != "application/json" { 175 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, "Unserviceable content type", nil, nil) 176 | return 177 | } 178 | 179 | // Read the body into byte array 180 | body, err := ioutil.ReadAll(r.Body) 181 | if err != nil { 182 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 183 | return 184 | } 185 | 186 | // Parse the body into Request 187 | authReq := &RequestWith2FA{} 188 | err = json.Unmarshal(body, authReq) 189 | if err != nil { 190 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 191 | return 192 | } 193 | 194 | // Get user by said email 195 | user, err := UserRepo.GetUserByEmail(r.Context(), authReq.Email) 196 | if err != nil || user == nil { 197 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), nil, nil) 198 | return 199 | } 200 | user.LastLogin = time.Now() 201 | 202 | // Make sure chages to this user are saved. 203 | defer UserRepo.UpdateUser(r.Context(), user) 204 | 205 | // Make sure the user is enabled 206 | if !user.Enabled { 207 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "account disabled", nil, nil) 208 | return 209 | } 210 | 211 | // Make sure the user is not suspended 212 | if user.Suspended { 213 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "account suspended", nil, nil) 214 | return 215 | } 216 | 217 | // Validate the user's password 218 | err = bcrypt.CompareHashAndPassword([]byte(user.HashedPassphrase), []byte(authReq.Passphrase)) 219 | if err != nil { 220 | user.FailCount++ 221 | if user.FailCount > 3 { 222 | user.Suspended = true 223 | } 224 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "email or passphrase not match", nil, nil) 225 | return 226 | } 227 | 228 | codes, err := UserRepo.GetTOTPRecoveryCodes(r.Context(), user) 229 | if err != nil { 230 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 231 | return 232 | } 233 | codeCorrect := false 234 | for _, v := range codes { 235 | if v == authReq.SecretKey { 236 | codeCorrect = true 237 | break 238 | } 239 | } 240 | if !codeCorrect { 241 | user.FailCount++ 242 | if user.FailCount > 3 { 243 | user.Suspended = true 244 | } 245 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "invalid secret key", nil, nil) 246 | return 247 | } 248 | 249 | _ = UserRepo.MarkTOTPRecoveryCodeUsed(r.Context(), user, authReq.SecretKey) 250 | 251 | // If the password is valid, reset the user's FailCount 252 | user.FailCount = 0 253 | 254 | var roles []string 255 | 256 | // Add user's role from direct UserRole relation. 257 | userRoles, _, err := UserRepo.ListAllUserRoles(r.Context(), user, &helper.PageRequest{ 258 | No: 1, 259 | PageSize: 1000, 260 | OrderBy: "ROLE_NAME", 261 | Sort: "ASC", 262 | }) 263 | if err != nil { 264 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 265 | return 266 | } 267 | // Add user's role into Token audiences info. 268 | roles = make([]string, len(userRoles)) 269 | for k, v := range userRoles { 270 | r, err := RoleRepo.GetRoleByRecID(r.Context(), v.RecID) 271 | if err == nil { 272 | roles[k] = fmt.Sprintf("%s@%s", r.RoleName, r.RoleDomain) 273 | } 274 | } 275 | 276 | // Set the account email into Token subject. 277 | subject := user.Email 278 | 279 | // Set the audience 280 | audience := roles 281 | 282 | access, refresh, err := TokenFactory.CreateTokenPair(subject, audience, nil) 283 | 284 | resp := &Response{ 285 | AccessToken: access, 286 | RefreshToken: refresh, 287 | } 288 | 289 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Successful", nil, resp) 290 | } 291 | 292 | // Authentication serve normal authentication 293 | func Authentication(w http.ResponseWriter, r *http.Request) { 294 | // Check content-type, make sure its application/json 295 | cType := r.Header.Get("Content-Type") 296 | if cType != "application/json" { 297 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, "Unserviceable content type", nil, nil) 298 | return 299 | } 300 | 301 | // Read the body into byte array 302 | body, err := ioutil.ReadAll(r.Body) 303 | if err != nil { 304 | helper.WriteHTTPResponse(r.Context(), w, http.StatusInternalServerError, err.Error(), nil, nil) 305 | return 306 | } 307 | 308 | // Parse the body into Request 309 | authReq := &Request{} 310 | err = json.Unmarshal(body, authReq) 311 | if err != nil { 312 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 313 | return 314 | } 315 | 316 | // Get user by said email 317 | user, err := UserRepo.GetUserByEmail(r.Context(), authReq.Email) 318 | if err != nil || user == nil { 319 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), nil, nil) 320 | return 321 | } 322 | 323 | user.LastLogin = time.Unix(time.Now().Unix(), 0) 324 | 325 | // Make sure chages to this user are saved. 326 | //defer func() { 327 | // err = UserRepo.UpdateUser(r.Context(), user) 328 | // if err != nil { 329 | // fmt.Println("Ouch", err.Error()) 330 | // } 331 | //}() 332 | 333 | // Make sure the user is enabled 334 | if !user.Enabled { 335 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "account disabled", nil, nil) 336 | err = UserRepo.UpdateUser(r.Context(), user) 337 | if err != nil { 338 | fmt.Println("Ouch", err.Error()) 339 | } 340 | return 341 | } 342 | 343 | // Make sure the user is not suspended 344 | if user.Suspended { 345 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "account suspended", nil, nil) 346 | err = UserRepo.UpdateUser(r.Context(), user) 347 | if err != nil { 348 | fmt.Println("Ouch", err.Error()) 349 | } 350 | return 351 | } 352 | 353 | // Validate the user's password 354 | err = bcrypt.CompareHashAndPassword([]byte(user.HashedPassphrase), []byte(authReq.Passphrase)) 355 | if err != nil { 356 | user.FailCount++ 357 | if user.FailCount > 3 { 358 | user.Suspended = true 359 | } 360 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "email or passphrase not match", nil, nil) 361 | err = UserRepo.UpdateUser(r.Context(), user) 362 | if err != nil { 363 | fmt.Println("Ouch", err.Error()) 364 | } 365 | return 366 | } 367 | 368 | if user.Enable2FactorAuth { 369 | user.Token2FA = helper.MakeRandomString(6, true, true, true, false) 370 | ret := make(map[string]string) 371 | ret["2FA_token"] = user.Token2FA 372 | helper.WriteHTTPResponse(r.Context(), w, http.StatusAccepted, "2FA needed", nil, ret) 373 | err = UserRepo.UpdateUser(r.Context(), user) 374 | if err != nil { 375 | fmt.Println("Ouch", err.Error()) 376 | } 377 | return 378 | } 379 | 380 | // If the password is valid, reset the user's FailCount 381 | user.FailCount = 0 382 | 383 | var roles []string 384 | 385 | // Add user's role from direct UserRole relation. 386 | userRoles, _, err := UserRepo.ListAllUserRoles(r.Context(), user, &helper.PageRequest{ 387 | No: 1, 388 | PageSize: 1000, 389 | OrderBy: "ROLE_NAME", 390 | Sort: "ASC", 391 | }) 392 | if err != nil { 393 | helper.WriteHTTPResponse(r.Context(), w, http.StatusBadRequest, err.Error(), nil, nil) 394 | err = UserRepo.UpdateUser(r.Context(), user) 395 | if err != nil { 396 | fmt.Println("Ouch", err.Error()) 397 | } 398 | return 399 | } 400 | 401 | // Add user's role into Token audiences info. 402 | roles = make([]string, len(userRoles)) 403 | for k, v := range userRoles { 404 | r, err := RoleRepo.GetRoleByRecID(r.Context(), v.RecID) 405 | if err == nil { 406 | roles[k] = fmt.Sprintf("%s@%s", r.RoleName, r.RoleDomain) 407 | } 408 | } 409 | 410 | // Set the account email into Token subject. 411 | subject := user.Email 412 | 413 | RevocationRepo.UnRevoke(r.Context(), subject) 414 | 415 | // Set the audience 416 | audience := roles 417 | 418 | access, refresh, err := TokenFactory.CreateTokenPair(subject, audience, nil) 419 | 420 | resp := &Response{ 421 | AccessToken: access, 422 | RefreshToken: refresh, 423 | } 424 | 425 | helper.WriteHTTPResponse(r.Context(), w, http.StatusOK, "Successful", nil, resp) 426 | err = UserRepo.UpdateUser(r.Context(), user) 427 | if err != nil { 428 | fmt.Println("Ouch", err.Error()) 429 | } 430 | } 431 | 432 | // Refresh serves token refresh 433 | func Refresh(w http.ResponseWriter, r *http.Request) { 434 | auth := r.Header.Get("Authorization") 435 | if len(auth) == 0 { 436 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "missing authentication header", nil, nil) 437 | return 438 | } 439 | 440 | // bearer 441 | if len(auth) < 6 || strings.ToUpper(auth[:6]) != "BEARER" { 442 | helper.WriteHTTPResponse(r.Context(), w, http.StatusUnauthorized, "invalid authentication method", nil, nil) 443 | return 444 | } 445 | 446 | // Token 447 | token := strings.TrimSpace(auth[7:]) 448 | 449 | ht, err := TokenFactory.ReadToken(token) 450 | revoked, err := RevocationRepo.IsRevoked(r.Context(), ht.Subject) 451 | if err != nil || revoked { 452 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, "your access been revoked, please authenticate again", nil, nil) 453 | return 454 | } 455 | 456 | access, err := TokenFactory.RefreshToken(token) 457 | if err != nil { 458 | helper.WriteHTTPResponse(r.Context(), w, http.StatusForbidden, err.Error(), nil, nil) 459 | return 460 | } 461 | 462 | resp := &RefreshResponse{AccessToken: access} 463 | 464 | helper.WriteHTTPResponse(r.Context(), w, 200, "access Token refreshed", nil, resp) 465 | } 466 | -------------------------------------------------------------------------------- /pkg/helper/DoubleStar.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | "unicode/utf8" 10 | ) 11 | 12 | // ErrBadPattern indicates a pattern was malformed. 13 | var ErrBadPattern = path.ErrBadPattern 14 | 15 | // Split a path on the given separator, respecting escaping. 16 | func splitPathOnSeparator(path string, separator rune) (ret []string) { 17 | idx := 0 18 | if separator == '\\' { 19 | // if the separator is '\\', then we can just split... 20 | ret = strings.Split(path, string(separator)) 21 | idx = len(ret) 22 | } else { 23 | // otherwise, we need to be careful of situations where the separator was escaped 24 | cnt := strings.Count(path, string(separator)) 25 | if cnt == 0 { 26 | return []string{path} 27 | } 28 | 29 | ret = make([]string, cnt+1) 30 | pathlen := len(path) 31 | separatorLen := utf8.RuneLen(separator) 32 | emptyEnd := false 33 | for start := 0; start < pathlen; { 34 | end := indexRuneWithEscaping(path[start:], separator) 35 | if end == -1 { 36 | emptyEnd = false 37 | end = pathlen 38 | } else { 39 | emptyEnd = true 40 | end += start 41 | } 42 | ret[idx] = path[start:end] 43 | start = end + separatorLen 44 | idx++ 45 | } 46 | 47 | // If the last rune is a path separator, we need to append an empty string to 48 | // represent the last, empty path component. By default, the strings from 49 | // make([]string, ...) will be empty, so we just need to icrement the count 50 | if emptyEnd { 51 | idx++ 52 | } 53 | } 54 | 55 | return ret[:idx] 56 | } 57 | 58 | // Find the first index of a rune in a string, 59 | // ignoring any times the rune is escaped using "\". 60 | func indexRuneWithEscaping(s string, r rune) int { 61 | end := strings.IndexRune(s, r) 62 | if end == -1 { 63 | return -1 64 | } 65 | if end > 0 && s[end-1] == '\\' { 66 | start := end + utf8.RuneLen(r) 67 | end = indexRuneWithEscaping(s[start:], r) 68 | if end != -1 { 69 | end += start 70 | } 71 | } 72 | return end 73 | } 74 | 75 | // Match returns true if name matches the shell file name pattern. 76 | // The pattern syntax is: 77 | // 78 | // pattern: 79 | // { term } 80 | // term: 81 | // '*' matches any sequence of non-path-separators 82 | // '**' matches any sequence of characters, including 83 | // path separators. 84 | // '?' matches any single non-path-separator character 85 | // '[' [ '^' ] { character-range } ']' 86 | // character class (must be non-empty) 87 | // '{' { term } [ ',' { term } ... ] '}' 88 | // c matches character c (c != '*', '?', '\\', '[') 89 | // '\\' c matches character c 90 | // 91 | // character-range: 92 | // c matches character c (c != '\\', '-', ']') 93 | // '\\' c matches character c 94 | // lo '-' hi matches character c for lo <= c <= hi 95 | // 96 | // Match requires pattern to match all of name, not just a substring. 97 | // The path-separator defaults to the '/' character. The only possible 98 | // returned error is ErrBadPattern, when pattern is malformed. 99 | // 100 | // Note: this is meant as a drop-in replacement for path.Match() which 101 | // always uses '/' as the path separator. If you want to support systems 102 | // which use a different path separator (such as Windows), what you want 103 | // is the PathMatch() function below. 104 | // 105 | func Match(pattern, name string) (bool, error) { 106 | return matchWithSeparator(pattern, name, '/') 107 | } 108 | 109 | // PathMatch is like Match except that it uses your system's path separator. 110 | // For most systems, this will be '/'. However, for Windows, it would be '\\'. 111 | // Note that for systems where the path separator is '\\', escaping is 112 | // disabled. 113 | // 114 | // Note: this is meant as a drop-in replacement for filepath.Match(). 115 | // 116 | func PathMatch(pattern, name string) (bool, error) { 117 | return matchWithSeparator(pattern, name, os.PathSeparator) 118 | } 119 | 120 | // Match returns true if name matches the shell file name pattern. 121 | // The pattern syntax is: 122 | // 123 | // pattern: 124 | // { term } 125 | // term: 126 | // '*' matches any sequence of non-path-separators 127 | // '**' matches any sequence of characters, including 128 | // path separators. 129 | // '?' matches any single non-path-separator character 130 | // '[' [ '^' ] { character-range } ']' 131 | // character class (must be non-empty) 132 | // '{' { term } [ ',' { term } ... ] '}' 133 | // c matches character c (c != '*', '?', '\\', '[') 134 | // '\\' c matches character c 135 | // 136 | // character-range: 137 | // c matches character c (c != '\\', '-', ']') 138 | // '\\' c matches character c, unless separator is '\\' 139 | // lo '-' hi matches character c for lo <= c <= hi 140 | // 141 | // Match requires pattern to match all of name, not just a substring. 142 | // The only possible returned error is ErrBadPattern, when pattern 143 | // is malformed. 144 | // 145 | func matchWithSeparator(pattern, name string, separator rune) (bool, error) { 146 | patternComponents := splitPathOnSeparator(pattern, separator) 147 | nameComponents := splitPathOnSeparator(name, separator) 148 | return doMatching(patternComponents, nameComponents) 149 | } 150 | 151 | func doMatching(patternComponents, nameComponents []string) (matched bool, err error) { 152 | // check for some base-cases 153 | patternLen, nameLen := len(patternComponents), len(nameComponents) 154 | if patternLen == 0 && nameLen == 0 { 155 | return true, nil 156 | } 157 | if patternLen == 0 || nameLen == 0 { 158 | return false, nil 159 | } 160 | 161 | patIdx, nameIdx := 0, 0 162 | for patIdx < patternLen && nameIdx < nameLen { 163 | if patternComponents[patIdx] == "**" { 164 | // if our last pattern component is a doublestar, we're done - 165 | // doublestar will match any remaining name components, if any. 166 | if patIdx++; patIdx >= patternLen { 167 | return true, nil 168 | } 169 | 170 | // otherwise, try matching remaining components 171 | for ; nameIdx < nameLen; nameIdx++ { 172 | if m, _ := doMatching(patternComponents[patIdx:], nameComponents[nameIdx:]); m { 173 | return true, nil 174 | } 175 | } 176 | return false, nil 177 | } 178 | 179 | // try matching components 180 | matched, err = matchComponent(patternComponents[patIdx], nameComponents[nameIdx]) 181 | if !matched || err != nil { 182 | return 183 | } 184 | 185 | patIdx++ 186 | nameIdx++ 187 | } 188 | return patIdx >= patternLen && nameIdx >= nameLen, nil 189 | } 190 | 191 | // Glob returns the names of all files matching pattern or nil 192 | // if there is no matching file. The syntax of pattern is the same 193 | // as in Match. The pattern may describe hierarchical names such as 194 | // /usr/*/bin/ed (assuming the Separator is '/'). 195 | // 196 | // Glob ignores file system errors such as I/O errors reading directories. 197 | // The only possible returned error is ErrBadPattern, when pattern 198 | // is malformed. 199 | // 200 | // Your system path separator is automatically used. This means on 201 | // systems where the separator is '\\' (Windows), escaping will be 202 | // disabled. 203 | // 204 | // Note: this is meant as a drop-in replacement for filepath.Glob(). 205 | // 206 | func Glob(pattern string) (matches []string, err error) { 207 | patternComponents := splitPathOnSeparator(filepath.ToSlash(pattern), '/') 208 | if len(patternComponents) == 0 { 209 | return nil, nil 210 | } 211 | 212 | // On Windows systems, this will return the drive name ('C:') for filesystem 213 | // paths, or \\\ for UNC paths. On other systems, it will 214 | // return an empty string. Since absolute paths on non-Windows systems start 215 | // with a slash, patternComponent[0] == volumeName will return true for both 216 | // absolute Windows paths and absolute non-Windows paths, but we need a 217 | // separate check for UNC paths. 218 | volumeName := filepath.VolumeName(pattern) 219 | isWindowsUNC := strings.HasPrefix(pattern, `\\`) 220 | if isWindowsUNC || patternComponents[0] == volumeName { 221 | startComponentIndex := 1 222 | if isWindowsUNC { 223 | startComponentIndex = 4 224 | } 225 | return doGlob(fmt.Sprintf("%s%s", volumeName, string(os.PathSeparator)), patternComponents[startComponentIndex:], matches) 226 | } 227 | 228 | // otherwise, it's a relative pattern 229 | return doGlob(".", patternComponents, matches) 230 | } 231 | 232 | // Perform a glob 233 | func doGlob(basedir string, components, matches []string) (m []string, e error) { 234 | m = matches 235 | e = nil 236 | 237 | // figure out how many components we don't need to glob because they're 238 | // just names without patterns - we'll use os.Lstat below to check if that 239 | // path actually exists 240 | patLen := len(components) 241 | patIdx := 0 242 | for ; patIdx < patLen; patIdx++ { 243 | if strings.IndexAny(components[patIdx], "*?[{\\") >= 0 { 244 | break 245 | } 246 | } 247 | if patIdx > 0 { 248 | basedir = filepath.Join(basedir, filepath.Join(components[0:patIdx]...)) 249 | } 250 | 251 | // Lstat will return an error if the file/directory doesn't exist 252 | fi, err := os.Lstat(basedir) 253 | if err != nil { 254 | return 255 | } 256 | 257 | // if there are no more components, we've found a match 258 | if patIdx >= patLen { 259 | m = append(m, basedir) 260 | return 261 | } 262 | 263 | // otherwise, we need to check each item in the directory... 264 | // first, if basedir is a symlink, follow it... 265 | if (fi.Mode() & os.ModeSymlink) != 0 { 266 | fi, err = os.Stat(basedir) 267 | if err != nil { 268 | return 269 | } 270 | } 271 | 272 | // confirm it's a directory... 273 | if !fi.IsDir() { 274 | return 275 | } 276 | 277 | // read directory 278 | dir, err := os.Open(basedir) 279 | if err != nil { 280 | return 281 | } 282 | defer dir.Close() 283 | 284 | files, _ := dir.Readdir(-1) 285 | lastComponent := (patIdx + 1) >= patLen 286 | if components[patIdx] == "**" { 287 | // if the current component is a doublestar, we'll try depth-first 288 | for _, file := range files { 289 | // if symlink, we may want to follow 290 | if (file.Mode() & os.ModeSymlink) != 0 { 291 | file, err = os.Stat(filepath.Join(basedir, file.Name())) 292 | if err != nil { 293 | continue 294 | } 295 | } 296 | 297 | if file.IsDir() { 298 | // recurse into directories 299 | if lastComponent { 300 | m = append(m, filepath.Join(basedir, file.Name())) 301 | } 302 | m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx:], m) 303 | } else if lastComponent { 304 | // if the pattern's last component is a doublestar, we match filenames, too 305 | m = append(m, filepath.Join(basedir, file.Name())) 306 | } 307 | } 308 | if lastComponent { 309 | return // we're done 310 | } 311 | patIdx++ 312 | lastComponent = (patIdx + 1) >= patLen 313 | } 314 | 315 | // check items in current directory and recurse 316 | var match bool 317 | for _, file := range files { 318 | match, e = matchComponent(components[patIdx], file.Name()) 319 | if e != nil { 320 | return 321 | } 322 | if match { 323 | if lastComponent { 324 | m = append(m, filepath.Join(basedir, file.Name())) 325 | } else { 326 | m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx+1:], m) 327 | } 328 | } 329 | } 330 | return 331 | } 332 | 333 | // Attempt to match a single pattern component with a path component 334 | func matchComponent(pattern, name string) (bool, error) { 335 | // check some base cases 336 | patternLen, nameLen := len(pattern), len(name) 337 | if patternLen == 0 && nameLen == 0 { 338 | return true, nil 339 | } 340 | if patternLen == 0 { 341 | return false, nil 342 | } 343 | if nameLen == 0 && pattern != "*" { 344 | return false, nil 345 | } 346 | 347 | // check for matches one rune at a time 348 | patIdx, nameIdx := 0, 0 349 | for patIdx < patternLen && nameIdx < nameLen { 350 | patRune, patAdj := utf8.DecodeRuneInString(pattern[patIdx:]) 351 | nameRune, nameAdj := utf8.DecodeRuneInString(name[nameIdx:]) 352 | if patRune == '\\' { 353 | // handle escaped runes 354 | patIdx += patAdj 355 | patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:]) 356 | if patRune == utf8.RuneError { 357 | return false, ErrBadPattern 358 | } else if patRune == nameRune { 359 | patIdx += patAdj 360 | nameIdx += nameAdj 361 | } else { 362 | return false, nil 363 | } 364 | } else if patRune == '*' { 365 | // handle stars 366 | if patIdx += patAdj; patIdx >= patternLen { 367 | // a star at the end of a pattern will always 368 | // match the rest of the path 369 | return true, nil 370 | } 371 | 372 | // check if we can make any matches 373 | for ; nameIdx < nameLen; nameIdx += nameAdj { 374 | if m, _ := matchComponent(pattern[patIdx:], name[nameIdx:]); m { 375 | return true, nil 376 | } 377 | } 378 | return false, nil 379 | } else if patRune == '[' { 380 | // handle character sets 381 | patIdx += patAdj 382 | endClass := indexRuneWithEscaping(pattern[patIdx:], ']') 383 | if endClass == -1 { 384 | return false, ErrBadPattern 385 | } 386 | endClass += patIdx 387 | classRunes := []rune(pattern[patIdx:endClass]) 388 | classRunesLen := len(classRunes) 389 | if classRunesLen > 0 { 390 | classIdx := 0 391 | matchClass := false 392 | if classRunes[0] == '^' { 393 | classIdx++ 394 | } 395 | for classIdx < classRunesLen { 396 | low := classRunes[classIdx] 397 | if low == '-' { 398 | return false, ErrBadPattern 399 | } 400 | classIdx++ 401 | if low == '\\' { 402 | if classIdx < classRunesLen { 403 | low = classRunes[classIdx] 404 | classIdx++ 405 | } else { 406 | return false, ErrBadPattern 407 | } 408 | } 409 | high := low 410 | if classIdx < classRunesLen && classRunes[classIdx] == '-' { 411 | // we have a range of runes 412 | if classIdx++; classIdx >= classRunesLen { 413 | return false, ErrBadPattern 414 | } 415 | high = classRunes[classIdx] 416 | if high == '-' { 417 | return false, ErrBadPattern 418 | } 419 | classIdx++ 420 | if high == '\\' { 421 | if classIdx < classRunesLen { 422 | high = classRunes[classIdx] 423 | classIdx++ 424 | } else { 425 | return false, ErrBadPattern 426 | } 427 | } 428 | } 429 | if low <= nameRune && nameRune <= high { 430 | matchClass = true 431 | } 432 | } 433 | if matchClass == (classRunes[0] == '^') { 434 | return false, nil 435 | } 436 | } else { 437 | return false, ErrBadPattern 438 | } 439 | patIdx = endClass + 1 440 | nameIdx += nameAdj 441 | } else if patRune == '{' { 442 | // handle alternatives such as {alt1,alt2,...} 443 | patIdx += patAdj 444 | endOptions := indexRuneWithEscaping(pattern[patIdx:], '}') 445 | if endOptions == -1 { 446 | return false, ErrBadPattern 447 | } 448 | endOptions += patIdx 449 | options := splitPathOnSeparator(pattern[patIdx:endOptions], ',') 450 | patIdx = endOptions + 1 451 | for _, o := range options { 452 | m, e := matchComponent(o+pattern[patIdx:], name[nameIdx:]) 453 | if e != nil { 454 | return false, e 455 | } 456 | if m { 457 | return true, nil 458 | } 459 | } 460 | return false, nil 461 | } else if patRune == '?' || patRune == nameRune { 462 | // handle single-rune wildcard 463 | patIdx += patAdj 464 | nameIdx += nameAdj 465 | } else { 466 | return false, nil 467 | } 468 | } 469 | if patIdx >= patternLen && nameIdx >= nameLen { 470 | return true, nil 471 | } 472 | if nameIdx >= nameLen && pattern[patIdx:] == "*" || pattern[patIdx:] == "**" { 473 | return true, nil 474 | } 475 | return false, nil 476 | } 477 | --------------------------------------------------------------------------------