├── 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 |
5 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
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 |
--------------------------------------------------------------------------------