├── internal ├── services │ └── v1 │ │ ├── tokens │ │ ├── models │ │ │ └── token_model.go │ │ ├── repository │ │ │ └── itoken.go │ │ └── service │ │ │ └── token.go │ │ ├── authentications │ │ ├── repository │ │ │ └── iauthentication.go │ │ ├── models │ │ │ └── authentication_model.go │ │ └── service │ │ │ └── utils.go │ │ ├── tenants │ │ ├── repository │ │ │ └── itenant.go │ │ └── models │ │ │ └── tenant_model.go │ │ ├── entitlements │ │ ├── repository │ │ │ └── ientitlement.go │ │ └── models │ │ │ └── entitlement_model.go │ │ ├── products │ │ ├── repository │ │ │ └── iproduct.go │ │ └── models │ │ │ └── product_model.go │ │ ├── accounts │ │ ├── repository │ │ │ └── iaccount.go │ │ ├── service │ │ │ └── utils.go │ │ └── models │ │ │ └── account_model.go │ │ ├── licenses │ │ ├── repository │ │ │ └── ilicense.go │ │ └── models │ │ │ └── license_model.go │ │ ├── machines │ │ ├── repository │ │ │ └── imachine.go │ │ └── models │ │ │ └── machine_model.go │ │ └── policies │ │ ├── repository │ │ └── ipolicy.go │ │ ├── models │ │ └── policy_model.go │ │ └── service │ │ └── utils.go ├── constants │ ├── app.go │ ├── custom_fields.go │ ├── heartbeat.go │ ├── cert.go │ ├── role_constants.go │ ├── account.go │ ├── machine.go │ ├── product_constants.go │ ├── datetime.go │ ├── license.go │ ├── http.go │ └── policy_constants.go ├── response │ ├── base.go │ └── response.go ├── utils │ ├── rand_test.go │ ├── tokens_test.go │ ├── pointers.go │ ├── encrypt_test.go │ ├── tokens.go │ ├── retry.go │ ├── compress.go │ ├── rand.go │ ├── rsa2048_pkcs1_test.go │ ├── net.go │ ├── encrypt.go │ ├── ed25519.go │ ├── rsa2048_pkcs1.go │ └── ed25519_test.go ├── repositories │ └── v1 │ │ ├── tokens │ │ └── repository.go │ │ ├── authentications │ │ └── repository.go │ │ ├── tenants │ │ └── repository.go │ │ ├── entitlements │ │ └── repository.go │ │ ├── products │ │ └── repository.go │ │ ├── accounts │ │ └── repository.go │ │ └── licenses │ │ └── repository.go ├── infrastructure │ ├── models │ │ ├── authentication_attribute │ │ │ └── struct.go │ │ ├── account_attribute │ │ │ └── struct.go │ │ ├── entitlement_attribute │ │ │ └── struct.go │ │ ├── license_attribute │ │ │ └── struct.go │ │ ├── product_attribute │ │ │ └── struct.go │ │ ├── machine_attribute │ │ │ └── struct.go │ │ └── policy_attribute │ │ │ └── struct.go │ ├── database │ │ ├── entities │ │ │ ├── role.go │ │ │ ├── key.go │ │ │ ├── tenant.go │ │ │ ├── entitlement.go │ │ │ ├── token.go │ │ │ ├── product.go │ │ │ ├── machine.go │ │ │ ├── account.go │ │ │ ├── license.go │ │ │ └── policy.go │ │ ├── sqlite │ │ │ └── client.go │ │ └── mysql │ │ │ └── client.go │ ├── logging │ │ └── logger.go │ ├── tracer │ │ └── tracer.go │ └── casbin_adapter │ │ └── xorm_adapter.go ├── middlewares │ ├── request_id_mw.go │ ├── recover_mw.go │ ├── response_hash_mw.go │ ├── timeout_mw.go │ ├── logger_mw.go │ ├── validate_permission_mw.go │ ├── machine_action_mw.go │ ├── account_action_mw.go │ └── license_action_mw.go ├── config │ └── config.go └── permissions │ └── permissions_test.go ├── conf ├── rbac_policy.csv ├── config.toml └── rbac_model.conf ├── Makefile ├── server ├── api │ ├── app_service.go │ ├── data_source.go │ ├── v1 │ │ ├── authentications │ │ │ ├── authentication_model.go │ │ │ └── authentication.go │ │ ├── tokens │ │ │ └── token.go │ │ ├── service.go │ │ ├── tenants │ │ │ └── tenant_model.go │ │ ├── entitlements │ │ │ └── entitlement_model.go │ │ ├── machines │ │ │ └── machine_model.go │ │ └── products │ │ │ └── product_model.go │ └── root_router.go └── server.go ├── .dockerignore ├── .gitignore ├── examples ├── casbin │ └── main.go ├── hash │ └── main.go ├── casbin_adapter │ └── main.go └── machine_fingerprint │ └── main.go ├── Dockerfile ├── LICENSE └── docker-compose.yaml /internal/services/v1/tokens/models/token_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | -------------------------------------------------------------------------------- /internal/constants/app.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const AppName = "go-license-management" 4 | -------------------------------------------------------------------------------- /internal/services/v1/tokens/repository/itoken.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | type IToken interface { 4 | } 5 | -------------------------------------------------------------------------------- /internal/constants/custom_fields.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | RequestIDField = "request_id" 5 | ) 6 | -------------------------------------------------------------------------------- /conf/rbac_policy.csv: -------------------------------------------------------------------------------- 1 | p, admin, data1, read 2 | p, admin, data1, write 3 | p, viewer, data1, read 4 | 5 | 6 | g, domain1, alice, admin 7 | g, test, user1, admin -------------------------------------------------------------------------------- /internal/constants/heartbeat.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | HeartbeatStatusNotStarted = iota 5 | HeartbeatStatusAlive 6 | HeartbeatStatusDead 7 | HeartbeatStatusResurrected 8 | ) 9 | -------------------------------------------------------------------------------- /internal/response/base.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type BaseOutput struct { 4 | Status int 5 | Code string 6 | Message string 7 | Data interface{} 8 | Count int 9 | } 10 | -------------------------------------------------------------------------------- /conf/config.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | mode="debug" 3 | 4 | [postgres] 5 | host="127.0.0.1" 6 | port="5432" 7 | username="postgres" 8 | password="123qweA#" 9 | database="licenses" 10 | 11 | [tracer] 12 | uri="127.0.0.1:4317" -------------------------------------------------------------------------------- /internal/utils/rand_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestRandStringBytesMaskImprSrcSB(t *testing.T) { 9 | fmt.Println(RandStringBytesMaskImprSrcSB(16)) 10 | } 11 | -------------------------------------------------------------------------------- /internal/utils/tokens_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGenerateKey(t *testing.T) { 9 | for _ = range 10 { 10 | fmt.Println(GenerateToken()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TAG := latest 2 | DOCKER_IMAGE := go-license-management:${TAG} 3 | 4 | build: 5 | docker build -t ${DOCKER_IMAGE} . 6 | 7 | 8 | push-local: 9 | docker push localhost:5000/${DOCKER_IMAGE} 10 | 11 | 12 | push-image: 13 | docker push ${DOCKER_IMAGE} -------------------------------------------------------------------------------- /internal/utils/pointers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func RefPointer[T any | interface{} | string | bool | int | int8 | int16 | int32 | int64 | float32 | float64](val T) *T { 4 | return &val 5 | } 6 | 7 | func DerefPointer[T any | interface{} | string | bool | int | int8 | int16 | int32 | int64 | float32 | float64](val *T) T { 8 | return *val 9 | } 10 | -------------------------------------------------------------------------------- /server/api/app_service.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | v1 "go-license-management/server/api/v1" 5 | ) 6 | 7 | type AppService struct { 8 | v1 *v1.V1AppService 9 | } 10 | 11 | func (svc *AppService) GetV1Svc() *v1.V1AppService { 12 | return svc.v1 13 | } 14 | 15 | func (svc *AppService) SetV1Svc(v1Svc *v1.V1AppService) { 16 | svc.v1 = v1Svc 17 | } 18 | -------------------------------------------------------------------------------- /conf/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = dom, sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) 12 | 13 | [matchers] 14 | m = g(r.dom, r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "superadmin" -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # The .dockerignore file excludes files from the container build process. 2 | # 3 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 4 | 5 | # Exclude locally vendored dependencies. 6 | vendor/ 7 | 8 | # Exclude "build-time" ignore files. 9 | .dockerignore 10 | .gcloudignore 11 | 12 | # Exclude git history and configuration. 13 | .gitignore -------------------------------------------------------------------------------- /internal/repositories/v1/tokens/repository.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "github.com/uptrace/bun" 5 | "go-license-management/server/api" 6 | ) 7 | 8 | type TokenRepository struct { 9 | database *bun.DB 10 | } 11 | 12 | func NewTokenRepository(ds *api.DataSource) *TokenRepository { 13 | return &TokenRepository{ 14 | database: ds.GetDatabase(), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/constants/cert.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | CertificatePEMFormat = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" 5 | PublicKeyPemFormat = "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----" 6 | ) 7 | 8 | const ( 9 | MachineFileFormat = "-----BEGIN MACHINE FILE-----\n%s\n-----END MACHINE FILE-----" 10 | LicenseFileFormat = "-----BEGIN LICENSE FILE-----\n%s\n-----END LICENSE FILE-----" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/infrastructure/models/authentication_attribute/struct.go: -------------------------------------------------------------------------------- 1 | package authentication_attribute 2 | 3 | import ( 4 | "go-license-management/internal/cerrors" 5 | ) 6 | 7 | type AuthenticationCommonURI struct { 8 | TenantName *string `uri:"tenant_name"` 9 | } 10 | 11 | func (req *AuthenticationCommonURI) Validate() error { 12 | if req.TenantName == nil { 13 | return cerrors.ErrTenantNameIsEmpty 14 | } 15 | 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /internal/middlewares/request_id_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | ) 8 | 9 | func RequestIDMW() gin.HandlerFunc { 10 | return func(ctx *gin.Context) { 11 | requestID := uuid.New().String() 12 | ctx.Request.Header.Set(constants.RequestIDField, requestID) 13 | ctx.Set(constants.RequestIDField, requestID) 14 | ctx.Next() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/role.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/uptrace/bun" 5 | "time" 6 | ) 7 | 8 | type Role struct { 9 | bun.BaseModel `bun:"table:roles,alias:r" swaggerignore:"true"` 10 | Name string `bun:"name,pk,type:varchar(256),notnull"` 11 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 12 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 13 | } 14 | -------------------------------------------------------------------------------- /internal/utils/encrypt_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestEncrypt(t *testing.T) { 10 | cypher, err := Encrypt([]byte("hehe"), []byte("secret")) 11 | assert.NoError(t, err) 12 | 13 | fmt.Println(string(cypher)) 14 | 15 | } 16 | 17 | func TestHashPassword(t *testing.T) { 18 | password := "abcd1234" 19 | hashed, err := HashPassword(password) 20 | assert.NoError(t, err) 21 | 22 | fmt.Println(hashed) 23 | } 24 | -------------------------------------------------------------------------------- /internal/services/v1/authentications/repository/iauthentication.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/infrastructure/database/entities" 6 | ) 7 | 8 | type IAuthentication interface { 9 | SelectTenantByPK(ctx context.Context, tenantName string) (*entities.Tenant, error) 10 | SelectAccountByPK(ctx context.Context, tenantName, username string) (*entities.Account, error) 11 | SelectMasterByPK(ctx context.Context, username string) (*entities.Master, error) 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /server/api/data_source.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | xormadapter "github.com/casbin/xorm-adapter/v3" 5 | "github.com/uptrace/bun" 6 | ) 7 | 8 | type DataSource struct { 9 | database *bun.DB 10 | casbin *xormadapter.Adapter 11 | } 12 | 13 | func (ds *DataSource) SetDatabase(db *bun.DB) { 14 | ds.database = db 15 | } 16 | 17 | func (ds *DataSource) GetDatabase() *bun.DB { 18 | return ds.database 19 | } 20 | 21 | func (ds *DataSource) SetCasbin(casbin *xormadapter.Adapter) { 22 | ds.casbin = casbin 23 | } 24 | 25 | func (ds *DataSource) GetCasbin() *xormadapter.Adapter { 26 | return ds.casbin 27 | } 28 | -------------------------------------------------------------------------------- /internal/infrastructure/database/sqlite/client.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/uptrace/bun" 6 | "github.com/uptrace/bun/dialect/sqlitedialect" 7 | "github.com/uptrace/bun/driver/sqliteshim" 8 | ) 9 | 10 | var sqliteClient *bun.DB 11 | 12 | func GetInstance() *bun.DB { 13 | return sqliteClient 14 | } 15 | 16 | func NewSqliteClient() (*bun.DB, error) { 17 | sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared") 18 | if err != nil { 19 | return nil, err 20 | } 21 | sqliteClient = bun.NewDB(sqldb, sqlitedialect.New()) 22 | 23 | return sqliteClient, nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/key.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/uptrace/bun" 6 | "time" 7 | ) 8 | 9 | type Key struct { 10 | bun.BaseModel `bun:"table:keys,alias:k"` 11 | 12 | ID uuid.UUID `bun:"id,pk,type:uuid"` 13 | Key string `bun:"key,nullzero"` 14 | PolicyID uuid.UUID `bun:"policy_id,type:uuid"` 15 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 16 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 17 | Policy *Policy `bun:"rel:belongs-to,join:policy_id=id"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/utils/tokens.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | func GenerateToken() string { 10 | const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 11 | const segmentLength = 8 12 | const totalLength = segmentLength * 5 13 | 14 | rd := rand.New(rand.NewSource(time.Now().UnixNano())) 15 | var buffer bytes.Buffer 16 | 17 | for i := 0; i < totalLength; i++ { 18 | buffer.WriteByte(charset[rd.Intn(len(charset))]) 19 | if (i+1)%segmentLength == 0 && i != totalLength-1 { 20 | buffer.WriteByte('-') 21 | } 22 | } 23 | 24 | return buffer.String() 25 | } 26 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/tenant.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/uptrace/bun" 5 | "time" 6 | ) 7 | 8 | type Tenant struct { 9 | bun.BaseModel `bun:"table:tenants,alias:tn" swaggerignore:"true"` 10 | 11 | Name string `bun:"name,pk,type:varchar(256),notnull"` 12 | Ed25519PublicKey string `bun:"ed25519_public_key,type:varchar(512),notnull"` 13 | Ed25519PrivateKey string `bun:"ed25519_private_key,type:varchar(512),notnull"` 14 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 15 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 16 | } 17 | -------------------------------------------------------------------------------- /internal/services/v1/authentications/models/authentication_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/infrastructure/models/authentication_attribute" 6 | "go.opentelemetry.io/otel/trace" 7 | ) 8 | 9 | type AuthenticationLoginInput struct { 10 | TracerCtx context.Context 11 | Tracer trace.Tracer 12 | authentication_attribute.AuthenticationCommonURI 13 | Username *string `json:"username" validate:"required" example:"test"` 14 | Password *string `json:"password" validate:"required" example:"test"` 15 | } 16 | 17 | type AuthenticationLoginOutput struct { 18 | Access string `json:"access"` 19 | ExpireAt int64 `json:"expire_at"` 20 | } 21 | -------------------------------------------------------------------------------- /internal/services/v1/tokens/service/token.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "go-license-management/internal/infrastructure/logging" 5 | "go-license-management/internal/services/v1/tokens/repository" 6 | ) 7 | 8 | type TokenService struct { 9 | repo repository.IToken 10 | logger *logging.Logger 11 | } 12 | 13 | func NewTokenService(options ...func(*TokenService)) *TokenService { 14 | svc := &TokenService{} 15 | 16 | for _, opt := range options { 17 | opt(svc) 18 | } 19 | logger := logging.NewECSLogger() 20 | svc.logger = logger 21 | 22 | return svc 23 | } 24 | 25 | func WithRepository(repo repository.IToken) func(*TokenService) { 26 | return func(c *TokenService) { 27 | c.repo = repo 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/constants/role_constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // RoleSuperAdmin is the role with the highest privileges. 5 | RoleSuperAdmin = "superadmin" 6 | // RoleAdmin is an admin role with privileges to perform most action (except those related to tenant management). 7 | // Should only be used server-side 8 | RoleAdmin = "admin" 9 | // RoleUser is the default role when creating account. Can be used client-side to communicate with server 10 | RoleUser = "user" 11 | ) 12 | 13 | var ValidRoleMapper = map[string]bool{ 14 | RoleSuperAdmin: true, 15 | RoleAdmin: true, 16 | RoleUser: true, 17 | } 18 | 19 | var ValidAccountCreationRoleMapper = map[string]bool{ 20 | RoleAdmin: true, 21 | RoleUser: true, 22 | } 23 | -------------------------------------------------------------------------------- /internal/constants/account.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | AccountStatusActive = "active" 5 | AccountStatusInactive = "inactive" 6 | AccountStatusBanned = "banned" 7 | ) 8 | 9 | const ( 10 | AccountActionUpdatePassword = "update-password" 11 | AccountActionResetPassword = "reset-password" 12 | AccountActionGenerateResetToken = "password-token" 13 | AccountActionBan = "ban" 14 | AccountActionUnban = "unban" 15 | ) 16 | 17 | var ValidAccountActionMapper = map[string]bool{ 18 | AccountActionUpdatePassword: true, 19 | AccountActionResetPassword: true, 20 | AccountActionGenerateResetToken: true, 21 | AccountActionBan: true, 22 | AccountActionUnban: true, 23 | } 24 | -------------------------------------------------------------------------------- /internal/services/v1/tenants/repository/itenant.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/constants" 6 | "go-license-management/internal/infrastructure/database/entities" 7 | ) 8 | 9 | type ITenant interface { 10 | InsertNewTenant(ctx context.Context, tenant *entities.Tenant) error 11 | SelectTenantByPK(ctx context.Context, name string) (*entities.Tenant, error) 12 | SelectTenants(ctx context.Context, queryParam constants.QueryCommonParam) ([]entities.Tenant, int, error) 13 | CheckTenantExistByPK(ctx context.Context, name string) (bool, error) 14 | DeleteTenantByPK(ctx context.Context, name string) error 15 | UpdateTenantByPK(ctx context.Context, tenant *entities.Tenant) (*entities.Tenant, error) 16 | } 17 | -------------------------------------------------------------------------------- /internal/infrastructure/models/account_attribute/struct.go: -------------------------------------------------------------------------------- 1 | package account_attribute 2 | 3 | import ( 4 | "go-license-management/internal/cerrors" 5 | "go-license-management/internal/constants" 6 | "go-license-management/internal/utils" 7 | ) 8 | 9 | type AccountCommonURI struct { 10 | TenantName *string `uri:"tenant_name"` 11 | Username *string `uri:"username"` 12 | Action *string `uri:"action"` 13 | } 14 | 15 | func (req *AccountCommonURI) Validate() error { 16 | if req.TenantName == nil { 17 | return cerrors.ErrTenantNameIsEmpty 18 | } 19 | 20 | if req.Action != nil { 21 | if _, ok := constants.ValidAccountActionMapper[utils.DerefPointer(req.Action)]; !ok { 22 | return cerrors.ErrAccountActionIsInvalid 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/constants/machine.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // MachineActionCheckout - Action to check out a machine. This will generate a snapshot 5 | // of the machine at time of checkout, encoded into a machine file certificate 6 | MachineActionCheckout = "check-out" 7 | 8 | // MachineActionPingHeartbeat - Action to ping server 9 | // to announce machine's alive status 10 | MachineActionPingHeartbeat = "ping-heartbeat" 11 | 12 | // MachineActionResetHeartbeat - Action to reset and stop 13 | // the machine's heartbeat monitor 14 | MachineActionResetHeartbeat = "reset-heartbeat" 15 | ) 16 | 17 | var ValidMachineActionsMapper = map[string]bool{ 18 | MachineActionCheckout: true, 19 | MachineActionPingHeartbeat: true, 20 | MachineActionResetHeartbeat: true, 21 | } 22 | -------------------------------------------------------------------------------- /internal/constants/product_constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // ProductDistributionStrategyOpen - Anybody can access releases. No API authentication required. 5 | ProductDistributionStrategyOpen = "open" 6 | // ProductDistributionStrategyClosed - Only admins can access releases. API authentication is required. 7 | ProductDistributionStrategyClosed = "closed" 8 | // ProductDistributionStrategyLicensed - Only licensed users, with a valid license, can access releases. API authentication is required. 9 | ProductDistributionStrategyLicensed = "licensed" 10 | ) 11 | 12 | var ValidProductDistributionStrategyMapper = map[string]bool{ 13 | ProductDistributionStrategyOpen: true, 14 | ProductDistributionStrategyClosed: true, 15 | ProductDistributionStrategyLicensed: true, 16 | } 17 | -------------------------------------------------------------------------------- /internal/infrastructure/models/entitlement_attribute/struct.go: -------------------------------------------------------------------------------- 1 | package entitlement_attribute 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/utils" 7 | ) 8 | 9 | type EntitlementCommonURI struct { 10 | TenantName *string `uri:"tenant_name" validate:"required" example:"test"` 11 | EntitlementID *string `uri:"entitlement_id" validate:"required" example:"test"` 12 | } 13 | 14 | func (req *EntitlementCommonURI) Validate() error { 15 | if req.TenantName == nil { 16 | return cerrors.ErrTenantNameIsEmpty 17 | } 18 | 19 | if req.EntitlementID != nil { 20 | if _, err := uuid.Parse(utils.DerefPointer(req.EntitlementID)); err != nil { 21 | return cerrors.ErrEntitlementIDIsInvalid 22 | } 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /examples/casbin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/casbin/casbin/v2" 6 | ) 7 | 8 | func main() { 9 | e, err := casbin.NewEnforcer("conf/rbac_model.conf", "conf/rbac_policy.csv") 10 | if err != nil { 11 | fmt.Println(err) 12 | return 13 | } 14 | 15 | subject := "alice" 16 | domain := "domain1" 17 | object := "data1" 18 | action := "write" 19 | 20 | // Check if the subject has permission 21 | ok, err := e.Enforce(domain, subject, object, action) 22 | if err != nil { 23 | fmt.Printf("Error checking permission: %v\n", err) 24 | return 25 | } 26 | if ok { 27 | fmt.Printf("Access granted for %s to %s %s in %s\n", subject, action, object, domain) 28 | } else { 29 | fmt.Printf("Access denied for %s to %s %s in %s\n", subject, action, object, domain) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/hash/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "fmt" 7 | ) 8 | 9 | type test struct { 10 | xx string 11 | } 12 | 13 | func main() { 14 | 15 | hash := sha256.Sum256([]byte(` 16 | { 17 | "request_id":"4f1684f1-dd10-4eaf-a7aa-2aefc52847c0", 18 | "code":"00000", 19 | "message":"OK", 20 | "server_time":1735866210, 21 | "data": { 22 | "username":"used1relu22g2dd4", 23 | "role_name":"user", 24 | "email":"122eel32d2gd2d3@gmail.com", 25 | "first_name":"dfec92f3-d60c-48e7-b123-a3e48bbe4829", 26 | "last_name":"user", 27 | "status":"active", 28 | "metadata":null, 29 | "created_at":"2025-01-03T08:03:30.897248571+07:00", 30 | "updated_at":"2025-01-03T08:03:30.897248571+07:00" 31 | } 32 | }`)) 33 | 34 | fmt.Println(hex.EncodeToString(hash[:])) 35 | } 36 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const SuperAdminUsername = "superadmin" 4 | 5 | const ( 6 | ServerMode = "server.mode" 7 | ServerHttpPort = "server.http_port" 8 | ServerEnableTLS = "server.enable_tls" 9 | ServerCertFile = "server.cert_file" 10 | ServerKeyFile = "server.key_file" 11 | ServerRequestTimeout = "server.request_timeout" 12 | ) 13 | 14 | const ( 15 | SuperAdminPassword = "superadmin.password" 16 | ) 17 | 18 | const ( 19 | TracerURI = "tracer.uri" 20 | ) 21 | 22 | const ( 23 | PostgresHost = "postgres.host" 24 | PostgresPort = "postgres.port" 25 | PostgresUsername = "postgres.username" 26 | PostgresPassword = "postgres.password" 27 | PostgresDatabase = "postgres.database" 28 | ) 29 | 30 | const ( 31 | AccessTokenTTL = "access_token.ttl" 32 | ) 33 | -------------------------------------------------------------------------------- /internal/utils/retry.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | // RetryWithBackoff retries the function call with waiting time between each retries 6 | func RetryWithBackoff(fn func() error, maxRetry int, startBackoff, maxBackoff time.Duration) { 7 | 8 | for attempt := 0; ; attempt++ { 9 | err := fn() 10 | if err == nil { 11 | return 12 | } 13 | if attempt == maxRetry-1 { 14 | return 15 | } 16 | 17 | time.Sleep(startBackoff) 18 | if startBackoff < maxBackoff { 19 | startBackoff *= 2 20 | } 21 | } 22 | } 23 | 24 | // Retry retries the function call without waiting time between each retries 25 | func Retry(fn func() error, maxRetry int) { 26 | for attempt := 0; ; attempt++ { 27 | err := fn() 28 | if err == nil { 29 | return 30 | } 31 | if attempt == maxRetry-1 { 32 | return 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/middlewares/recover_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/infrastructure/logging" 7 | "go-license-management/internal/response" 8 | "net/http" 9 | "runtime/debug" 10 | ) 11 | 12 | func Recovery() gin.HandlerFunc { 13 | return func(ctx *gin.Context) { 14 | defer func() { 15 | if err := recover(); err != nil { 16 | resp := response.NewResponse(ctx) 17 | logging.GetInstance().GetLogger().Error(string(debug.Stack())) 18 | resp.ToResponse(cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], err, nil, nil) 19 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, resp) 20 | return 21 | } 22 | }() 23 | 24 | ctx.Next() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/utils/compress.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io" 7 | ) 8 | 9 | func Compress(s []byte) ([]byte, error) { 10 | var err error 11 | 12 | buf := bytes.Buffer{} 13 | zipped := gzip.NewWriter(&buf) 14 | 15 | _, err = zipped.Write(s) 16 | if err != nil { 17 | return nil, err 18 | } 19 | err = zipped.Close() 20 | if err != nil { 21 | return nil, err 22 | } 23 | return buf.Bytes(), nil 24 | } 25 | 26 | func Decompress(s []byte) ([]byte, error) { 27 | rdr, err := gzip.NewReader(bytes.NewReader(s)) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer func() { 32 | cErr := rdr.Close() 33 | if cErr != nil && err == nil { 34 | err = cErr 35 | } 36 | }() 37 | 38 | data, err := io.ReadAll(rdr) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return data, nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/services/v1/entitlements/repository/ientitlement.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/database/entities" 8 | ) 9 | 10 | type IEntitlement interface { 11 | InsertNewEntitlement(ctx context.Context, entitlement *entities.Entitlement) error 12 | SelectTenantByPK(ctx context.Context, tenantName string) (*entities.Tenant, error) 13 | SelectEntitlementsByTenant(ctx context.Context, tenantName string, param constants.QueryCommonParam) ([]entities.Entitlement, int, error) 14 | SelectEntitlementByPK(ctx context.Context, entitlementID uuid.UUID) (*entities.Entitlement, error) 15 | CheckEntitlementExistByCode(ctx context.Context, code string) (bool, error) 16 | DeleteEntitlementByPK(ctx context.Context, entitlementID uuid.UUID) error 17 | } 18 | -------------------------------------------------------------------------------- /internal/middlewares/response_hash_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "go-license-management/internal/constants" 9 | ) 10 | 11 | func HashHeaderMW() gin.HandlerFunc { 12 | return func(ctx *gin.Context) { 13 | writer := &ResponseWriterInterceptor{ 14 | ResponseWriter: ctx.Writer, 15 | body: make([]byte, 0), 16 | } 17 | ctx.Writer = writer 18 | 19 | ctx.Next() 20 | 21 | } 22 | } 23 | 24 | type ResponseWriterInterceptor struct { 25 | gin.ResponseWriter 26 | body []byte 27 | } 28 | 29 | func (w *ResponseWriterInterceptor) Write(data []byte) (int, error) { 30 | w.body = append(w.body, data...) 31 | hash := sha256.Sum256(data) 32 | w.Header().Add(constants.ContentDigestHeader, fmt.Sprintf("sha256=%s", hex.EncodeToString(hash[:]))) 33 | return w.ResponseWriter.Write(data) 34 | } 35 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/entitlement.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/uptrace/bun" 6 | "time" 7 | ) 8 | 9 | type Entitlement struct { 10 | bun.BaseModel `bun:"table:entitlements,alias:e" swaggerignore:"true"` 11 | 12 | ID uuid.UUID `bun:"id,pk,type:uuid"` 13 | TenantName string `bun:"tenant_name,type:varchar(256),notnull"` 14 | Name string `bun:"name,type:varchar(256),notnull"` 15 | Code string `bun:"code,type:varchar(256),unique,notnull"` 16 | Metadata map[string]interface{} `bun:"metadata,type:jsonb,nullzero"` 17 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 18 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 19 | Tenant *Tenant `bun:"rel:belongs-to,join:tenant_name=name"` 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.4-alpine3.21 AS build 2 | 3 | RUN mkdir /opt/app 4 | WORKDIR /opt/app 5 | 6 | COPY ./*.go ./ 7 | COPY ./docs ./docs 8 | COPY ./internal ./internal 9 | COPY ./server ./server 10 | 11 | COPY go.mod go.sum ./ 12 | RUN go mod download 13 | 14 | RUN go install golang.org/x/vuln/cmd/govulncheck@latest 15 | RUN govulncheck ./... 16 | 17 | RUN go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest 18 | RUN fieldalignment -json -fix ./... 19 | 20 | WORKDIR /opt/app 21 | 22 | RUN go build -o go-license-management . 23 | 24 | FROM alpine:latest 25 | 26 | RUN mkdir /opt/app 27 | WORKDIR /opt/app 28 | 29 | RUN apk add tzdata 30 | ENV TZ=Asia/Ho_Chi_Minh 31 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 32 | 33 | COPY --from=build /opt/app/go-license-management /opt/app/go-license-management 34 | RUN mkdir ./conf 35 | RUN touch ./conf/config.toml 36 | 37 | CMD ["./go-license-management"] 38 | -------------------------------------------------------------------------------- /internal/services/v1/products/repository/iproduct.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/database/entities" 8 | ) 9 | 10 | type IProduct interface { 11 | InsertNewProduct(ctx context.Context, product *entities.Product) error 12 | InsertNewProductToken(ctx context.Context, productToken *entities.ProductToken) error 13 | UpdateProductByPK(ctx context.Context, product *entities.Product) error 14 | SelectTenantByPK(ctx context.Context, tenantName string) (*entities.Tenant, error) 15 | CheckProductExistByCode(ctx context.Context, code string) (bool, error) 16 | SelectProductByPK(ctx context.Context, productID uuid.UUID) (*entities.Product, error) 17 | SelectProducts(ctx context.Context, tenantName string, queryParam constants.QueryCommonParam) ([]entities.Product, int, error) 18 | DeleteProductByPK(ctx context.Context, productID uuid.UUID) error 19 | } 20 | -------------------------------------------------------------------------------- /internal/services/v1/accounts/repository/iaccount.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/constants" 6 | "go-license-management/internal/infrastructure/database/entities" 7 | ) 8 | 9 | type IAccount interface { 10 | InsertNewAccount(ctx context.Context, account *entities.Account) error 11 | UpdateAccountByPK(ctx context.Context, account *entities.Account) (*entities.Account, error) 12 | SelectTenantByPK(ctx context.Context, tenantName string) (*entities.Tenant, error) 13 | SelectAccountsByTenant(ctx context.Context, tenantName string, queryParam constants.QueryCommonParam) ([]entities.Account, int, error) 14 | SelectAccountByPK(ctx context.Context, tenantName, username string) (*entities.Account, error) 15 | CheckAccountExistByPK(ctx context.Context, tenantName, username string) (bool, error) 16 | CheckAccountEmailExistByPK(ctx context.Context, tenantName, email string) (bool, error) 17 | DeleteAccountByPK(ctx context.Context, tenantName, username string) error 18 | } 19 | -------------------------------------------------------------------------------- /internal/constants/datetime.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | DateFormatYMDHyphen = "2006-01-02" // yyyy-mm-dd 5 | DateFormatDMYHyphen = "02-01-2006" // dd-mm-yyyy 6 | DateFormatISO8601Hyphen = "2006-01-02T15:04:05.000Z" 7 | DateFormatYMDHMSHyphen = "2006-01-02 15:04:05" // yyyy-mm-dd hh:mm:ss 8 | DateFormatDMYHMSHyphen = "02-01-2006 15:04:05" // dd-mm-yyyy hh:mm:ss 9 | DateFormatHMSYMDHyphen = "15:04:05 2006-01-02" // hh:mm:ss yyyy-mm-dd 10 | DateFormatHMSDMYHyphen = "15:04:05 02-01-2006" // hh:mm:ss dd-mm-yyyy 11 | DateFormatYMDSlash = "2006/01/02" // yyyy/mm/dd 12 | DateFormatDMYSlash = "02/01/2006" // dd/mm/yyyy 13 | DateFormatYMDHMSSlash = "2006-01-02 15:04:05" // yyyy/mm/dd hh:mm:ss 14 | DateFormatDMYHMSSlash = "02/01/2006 15:04:05" // dd/mm/yyyy hh:mm:ss 15 | DateFormatHMSYMDSlash = "15:04:05 2006/01/02" // hh:mm:ss yyyy/mm/dd 16 | DateFormatHMSDMYSlash = "15:04:05 02/01/2006" // hh:mm:ss dd/mm/yyyy 17 | DateFormatHMS = "15:04:05" // hh:mm:ss 18 | ) 19 | -------------------------------------------------------------------------------- /examples/casbin_adapter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/casbin/casbin/v2" 6 | xormadapter "github.com/casbin/xorm-adapter/v3" 7 | _ "github.com/lib/pq" 8 | ) 9 | 10 | func main() { 11 | a, err := xormadapter.NewAdapter("postgres", "user=postgres password=123qweA# host=127.0.0.1 port=5432 sslmode=disable") 12 | if err != nil { 13 | fmt.Println(err) 14 | return 15 | } 16 | 17 | e, err := casbin.NewEnforcer("conf/rbac_model.conf", a) 18 | if err != nil { 19 | fmt.Println(err) 20 | return 21 | } 22 | 23 | err = e.LoadPolicy() 24 | if err != nil { 25 | fmt.Println(err) 26 | return 27 | } 28 | 29 | fmt.Println(e.GetAllDomains()) 30 | 31 | policies, err := e.GetPolicy() 32 | if err != nil { 33 | fmt.Println(err) 34 | return 35 | } 36 | 37 | fmt.Println("policies", policies) 38 | 39 | ok, err := e.Enforce("test", "user1", "product", "create") 40 | if err != nil { 41 | fmt.Printf("Error checking permission: %v\n", err) 42 | return 43 | } 44 | fmt.Println("result", ok) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /internal/utils/rand.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 10 | const ( 11 | letterIdxBits = 6 // 6 bits to represent a letter index 12 | letterIdxMask = 1<= 0; { 23 | if remain == 0 { 24 | cache, remain = src.Int63(), letterIdxMax 25 | } 26 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 27 | sb.WriteByte(letterBytes[idx]) 28 | i-- 29 | } 30 | cache >>= letterIdxBits 31 | remain-- 32 | } 33 | 34 | return sb.String() 35 | } 36 | -------------------------------------------------------------------------------- /internal/utils/rsa2048_pkcs1_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNewRSA2048KeyPair(t *testing.T) { 10 | privateKey, publicKey, err := NewRSA2048PKCS1KeyPair() 11 | assert.NoError(t, err) 12 | fmt.Println(privateKey) 13 | fmt.Println(publicKey) 14 | } 15 | 16 | func TestNewLicenseKeyWithRSA2048(t *testing.T) { 17 | privateKey, _, err := NewRSA2048PKCS1KeyPair() 18 | assert.NoError(t, err) 19 | 20 | licenseKey, err := NewLicenseKeyWithRSA2048PKCS1(privateKey, "dadada") 21 | assert.NoError(t, err) 22 | 23 | fmt.Println(licenseKey) 24 | } 25 | 26 | func TestVerifyLicenseKeyWithRSA2048(t *testing.T) { 27 | privateKey, publicKey, err := NewRSA2048PKCS1KeyPair() 28 | assert.NoError(t, err) 29 | fmt.Println(privateKey) 30 | fmt.Println(publicKey) 31 | 32 | licenseKey, err := NewLicenseKeyWithRSA2048PKCS1(privateKey, "dadada") 33 | assert.NoError(t, err) 34 | 35 | fmt.Println(licenseKey) 36 | 37 | valid, _, err := VerifyLicenseKeyWithRSA2048PKCS1(publicKey, licenseKey) 38 | assert.NoError(t, err) 39 | fmt.Println(valid) 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Thomas Pham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/token.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/uptrace/bun" 6 | "time" 7 | ) 8 | 9 | type ProductToken struct { 10 | bun.BaseModel `bun:"table:product_tokens,alias:pt" swaggerignore:"true"` 11 | 12 | ID uuid.UUID `bun:"id,pk,type:uuid"` 13 | ProductID uuid.UUID `bun:"product_id,type:uuid,notnull"` 14 | TenantName string `bun:"tenant_name,type:varchar(256),notnull"` 15 | Token string `bun:"token,type:varchar(128)"` 16 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 17 | Product *Product `bun:"rel:belongs-to,join:product_id=id"` 18 | } 19 | 20 | //type LicenseToken struct { 21 | // bun.BaseModel `bun:"table:license_tokens,alias:lt" swaggerignore:"true"` 22 | // 23 | // Token string `bun:"token,pk,type:varchar(256)"` 24 | // TenantName string `bun:"tenant_name,type:varchar(256),notnull"` 25 | // LicenseID uuid.UUID `bun:"license_id,type:uuid,notnull"` 26 | // CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 27 | // License *License `bun:"rel:belongs-to,join:license_id=id"` 28 | //} 29 | -------------------------------------------------------------------------------- /internal/infrastructure/models/license_attribute/struct.go: -------------------------------------------------------------------------------- 1 | package license_attribute 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/utils" 8 | ) 9 | 10 | type LicenseCommonURI struct { 11 | TenantName *string `uri:"tenant_name"` 12 | LicenseID *string `uri:"license_id"` 13 | Action *string `uri:"action"` 14 | } 15 | 16 | func (req *LicenseCommonURI) Validate() error { 17 | if req.TenantName == nil { 18 | return cerrors.ErrTenantNameIsEmpty 19 | } 20 | 21 | if req.LicenseID != nil { 22 | if _, err := uuid.Parse(utils.DerefPointer(req.LicenseID)); err != nil { 23 | return cerrors.ErrLicenseIDIsInvalid 24 | } 25 | } 26 | 27 | if req.Action != nil { 28 | if _, ok := constants.ValidLicenseActionMapper[utils.DerefPointer(req.Action)]; !ok { 29 | return cerrors.ErrLicenseActionIsInvalid 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // LicenseFileContent contains information about the license file 37 | type LicenseFileContent struct { 38 | Enc string `json:"enc"` 39 | Sig string `json:"sig"` 40 | Alg string `json:"alg"` 41 | } 42 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/product.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/uptrace/bun" 6 | "time" 7 | ) 8 | 9 | type Product struct { 10 | bun.BaseModel `bun:"table:products,alias:p" swaggerignore:"true"` 11 | 12 | ID uuid.UUID `bun:"id,pk,type:uuid"` 13 | TenantName string `bun:"tenant_name,type:varchar(256),notnull"` 14 | Name string `bun:"name,type:varchar(256)"` 15 | DistributionStrategy string `bun:"distribution_strategy,type:varchar(128)"` 16 | Code string `bun:"code,type:varchar(128),unique"` 17 | URL string `bun:"url,type:varchar(1024)"` 18 | Platforms []string `bun:"platform,type:jsonb"` 19 | Metadata map[string]interface{} `bun:"metadata,type:jsonb"` 20 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 21 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 22 | Tenant *Tenant `bun:"rel:belongs-to,join:tenant_name=name"` 23 | } 24 | -------------------------------------------------------------------------------- /internal/services/v1/licenses/repository/ilicense.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/database/entities" 8 | ) 9 | 10 | type ILicense interface { 11 | InsertNewLicense(ctx context.Context, license *entities.License) error 12 | SelectTenantByName(ctx context.Context, tenantName string) (*entities.Tenant, error) 13 | SelectProductByPK(ctx context.Context, productID uuid.UUID) (*entities.Product, error) 14 | SelectPolicyByPK(ctx context.Context, policyID uuid.UUID) (*entities.Policy, error) 15 | SelectLicenseByPK(ctx context.Context, licenseID uuid.UUID) (*entities.License, error) 16 | SelectLicenses(ctx context.Context, tenantName string, queryParam constants.QueryCommonParam) ([]entities.License, int, error) 17 | SelectLicenseByLicenseKey(ctx context.Context, licenseKey string) (*entities.License, error) 18 | DeleteLicenseByPK(ctx context.Context, licenseID uuid.UUID) error 19 | UpdateLicenseByPK(ctx context.Context, license *entities.License) (*entities.License, error) 20 | CheckPolicyExist(ctx context.Context, policyID uuid.UUID) (bool, error) 21 | CheckProductExist(ctx context.Context, productID uuid.UUID) (bool, error) 22 | } 23 | -------------------------------------------------------------------------------- /internal/middlewares/timeout_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/response" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func TimeoutMW() func(ctx *gin.Context) { 12 | return func(ctx *gin.Context) { 13 | 14 | timeoutDuration := 10 * time.Second 15 | 16 | finish := make(chan struct{}, 1) 17 | panicChan := make(chan interface{}, 1) 18 | 19 | go func() { 20 | defer func() { 21 | if p := recover(); p != nil { 22 | panicChan <- p 23 | } 24 | }() 25 | ctx.Next() 26 | finish <- struct{}{} 27 | }() 28 | 29 | resp := response.NewResponse(ctx) 30 | select { 31 | case <-panicChan: 32 | resp.ToResponse(cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], nil, nil, nil) 33 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, resp) 34 | return 35 | case <-time.After(timeoutDuration): 36 | resp.ToResponse(cerrors.ErrCodeMapper[cerrors.ErrGenericRequestTimedOut], cerrors.ErrMessageMapper[cerrors.ErrGenericRequestTimedOut], nil, nil, nil) 37 | ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, resp) 38 | return 39 | case <-finish: 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/middlewares/logger_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go-license-management/internal/constants" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "time" 9 | ) 10 | 11 | func LoggerMW(logger *zap.Logger) gin.HandlerFunc { 12 | return func(ctx *gin.Context) { 13 | start := time.Now() 14 | path := ctx.Request.URL.Path 15 | query := ctx.Request.URL.RawQuery 16 | ctx.Next() 17 | 18 | if len(ctx.Errors) > 0 { 19 | for _, e := range ctx.Errors.Errors() { 20 | logger.Error(e) 21 | } 22 | return 23 | } 24 | 25 | latency := time.Since(start).Milliseconds() 26 | fields := []zapcore.Field{ 27 | zap.String(constants.RequestIDField, ctx.GetString(constants.RequestIDField)), 28 | zap.Int("status", ctx.Writer.Status()), 29 | zap.String("method", ctx.Request.Method), 30 | zap.String("path", path), 31 | zap.String("message", path), 32 | zap.String("full-path", ctx.FullPath()), 33 | zap.String("query", query), 34 | zap.String("ip", ctx.ClientIP()), 35 | zap.String("user-agent", ctx.Request.UserAgent()), 36 | zap.Int64("latency", latency), 37 | zap.String(constants.ContextValueSubject, ctx.GetString(constants.ContextValueSubject)), 38 | } 39 | logger.Info("", fields...) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/api/v1/authentications/authentication_model.go: -------------------------------------------------------------------------------- 1 | package authentications 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/infrastructure/models/authentication_attribute" 7 | "go-license-management/internal/services/v1/authentications/models" 8 | "go.opentelemetry.io/otel/trace" 9 | ) 10 | 11 | type AuthenticationLoginRequest struct { 12 | Username *string `form:"username" validate:"required" example:"test"` 13 | Password *string `form:"password" validate:"required" example:"test"` 14 | } 15 | 16 | func (req *AuthenticationLoginRequest) Validate() error { 17 | if req.Username == nil { 18 | return cerrors.ErrAccountUsernameIsEmpty 19 | } 20 | 21 | if req.Password == nil { 22 | return cerrors.ErrAccountPasswordIsEmpty 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func (req *AuthenticationLoginRequest) ToAuthenticationLoginInput(ctx context.Context, tracer trace.Tracer, uriReq authentication_attribute.AuthenticationCommonURI) *models.AuthenticationLoginInput { 29 | return &models.AuthenticationLoginInput{ 30 | TracerCtx: ctx, 31 | Tracer: tracer, 32 | AuthenticationCommonURI: uriReq, 33 | Username: req.Username, 34 | Password: req.Password, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-license-management/internal/constants" 7 | "time" 8 | ) 9 | 10 | type Response struct { 11 | RequestID string `json:"request_id"` 12 | ErrorCode string `json:"code"` 13 | ErrorMessage string `json:"message"` 14 | ServerTime int64 `json:"server_time"` 15 | Count int `json:"count,omitempty"` 16 | Data interface{} `json:"data"` 17 | Agg interface{} `json:"agg,omitempty"` 18 | Meta interface{} `json:"meta,omitempty"` 19 | } 20 | 21 | func NewResponse(ctx context.Context) *Response { 22 | resp := new(Response) 23 | resp.RequestID = fmt.Sprintf("%v", ctx.Value(constants.RequestIDField)) 24 | resp.ServerTime = time.Now().Unix() 25 | resp.ErrorCode = "" 26 | resp.ErrorMessage = "" 27 | resp.Data = map[string]interface{}{} 28 | return resp 29 | } 30 | 31 | func (resp *Response) ToResponse(code, message string, data, meta, count interface{}) *Response { 32 | resp.ErrorCode = code 33 | resp.ErrorMessage = message 34 | 35 | if data != nil { 36 | resp.Data = data 37 | } 38 | 39 | if meta != nil { 40 | resp.Meta = meta 41 | } 42 | if count != nil { 43 | if _, ok := count.(int); ok { 44 | resp.Count = count.(int) 45 | } 46 | } 47 | return resp 48 | } 49 | -------------------------------------------------------------------------------- /internal/infrastructure/models/product_attribute/struct.go: -------------------------------------------------------------------------------- 1 | package product_attribute 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/utils" 7 | ) 8 | 9 | type ProductCommonURI struct { 10 | TenantName *string `uri:"tenant_name"` 11 | ProductID *string `uri:"product_id"` 12 | } 13 | 14 | func (req *ProductCommonURI) Validate() error { 15 | if req.TenantName == nil { 16 | return cerrors.ErrTenantNameIsEmpty 17 | } 18 | 19 | if req.ProductID != nil { 20 | if _, err := uuid.Parse(utils.DerefPointer(req.ProductID)); err != nil { 21 | return cerrors.ErrProductIDIsInvalid 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | 28 | type ProductAttribute struct { 29 | Name *string `json:"name" validate:"required" example:"test"` 30 | Code *string `json:"code" validate:"required" example:"test"` 31 | DistributionStrategy *string `json:"distribution_strategy" validate:"optional" example:"test"` 32 | Url *string `json:"url" validate:"optional" example:"test"` 33 | Permissions []string `json:"permissions" validate:"optional" example:"test"` 34 | Platforms []string `json:"platforms" validate:"optional" example:"test"` 35 | Metadata map[string]interface{} `json:"metadata" validate:"optional"` 36 | } 37 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/machine.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type Machine struct { 9 | ID uuid.UUID `bun:"id,pk,type:uuid"` 10 | LicenseID uuid.UUID `bun:"license_id,type:uuid,notnull"` 11 | LicenseKey string `bun:"license_key,type:varchar(256),notnull"` 12 | TenantName string `bun:"tenant_name,type:varchar(256),notnull"` 13 | Fingerprint string `bun:"fingerprint"` 14 | IP string `bun:"ip,type:varchar(64)"` 15 | Hostname string `bun:"hostname,type:varchar(128)"` 16 | Platform string `bun:"platform,type:varchar(128)"` 17 | Name string `bun:"name,type:varchar(128)"` 18 | Metadata map[string]interface{} `bun:"metadata,type:jsonb"` 19 | Cores int `bun:"cores,type:integer"` 20 | LastHeartbeatAt time.Time `bun:"last_heartbeat_at"` 21 | LastCheckOutAt time.Time `bun:"last_check_out_at"` 22 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 23 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 24 | Tenant *Tenant `bun:"rel:belongs-to,join:tenant_name=name"` 25 | License *License `bun:"rel:belongs-to,join:license_id=id"` 26 | } 27 | -------------------------------------------------------------------------------- /internal/infrastructure/database/mysql/client.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/go-sql-driver/mysql" 7 | "github.com/uptrace/bun" 8 | "github.com/uptrace/bun/dialect/mysqldialect" 9 | "time" 10 | ) 11 | 12 | const ( 13 | defaultTimeout = 10 * time.Second 14 | defaultReadTimeout = 10 * time.Second 15 | defaultWriteTimeout = 10 * time.Second 16 | defaultMaxIdleConn int = 100 17 | defaultMaxOpenConn int = 100 18 | ) 19 | 20 | var mysqlClient *bun.DB 21 | 22 | func GetInstance() *bun.DB { 23 | return mysqlClient 24 | } 25 | 26 | func NewMysqlClient(host, port, dbname, userName, password string) (*bun.DB, error) { 27 | 28 | config := &mysql.Config{ 29 | User: userName, 30 | Passwd: password, 31 | Net: "tcp", 32 | Addr: fmt.Sprintf("%s:%s", host, port), 33 | DBName: dbname, 34 | AllowNativePasswords: true, 35 | Timeout: defaultTimeout, 36 | ReadTimeout: defaultReadTimeout, 37 | WriteTimeout: defaultWriteTimeout, 38 | } 39 | 40 | mysqlDB, err := sql.Open("mysql", config.FormatDSN()) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | mysqlDB.SetMaxOpenConns(defaultMaxOpenConn) 46 | mysqlDB.SetMaxIdleConns(defaultMaxIdleConn) 47 | mysqlDB.SetConnMaxIdleTime(30 * time.Minute) 48 | mysqlDB.SetConnMaxLifetime(60 * time.Minute) 49 | 50 | mysqlClient = bun.NewDB(mysqlDB, mysqldialect.New()) 51 | 52 | return mysqlClient, nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/services/v1/tenants/models/tenant_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/constants" 6 | "go.opentelemetry.io/otel/trace" 7 | "time" 8 | ) 9 | 10 | type TenantRegistrationInput struct { 11 | TracerCtx context.Context 12 | Tracer trace.Tracer 13 | Name *string `json:"name,omitempty" validate:"required" example:"test"` 14 | } 15 | 16 | type TenantRegistrationOutput struct { 17 | Name string `json:"name"` 18 | CreatedAt time.Time `json:"created_at"` 19 | UpdatedAt time.Time `json:"updated_at"` 20 | } 21 | 22 | type TenantListInput struct { 23 | TracerCtx context.Context 24 | Tracer trace.Tracer 25 | constants.QueryCommonParam 26 | } 27 | 28 | type TenantRetrievalInput struct { 29 | TracerCtx context.Context 30 | Tracer trace.Tracer 31 | Name *string `json:"name,omitempty" validate:"required" example:"test"` 32 | } 33 | 34 | type TenantRetrievalOutput struct { 35 | Name string `json:"name"` 36 | Ed25519PublicKey string `json:"ed25519_public_key"` 37 | CreatedAt time.Time `json:"created_at"` 38 | UpdatedAt time.Time `json:"updated_at"` 39 | } 40 | 41 | type TenantDeletionInput struct { 42 | TracerCtx context.Context 43 | Tracer trace.Tracer 44 | Name *string `json:"name,omitempty" validate:"required" example:"test"` 45 | } 46 | 47 | type TenantRegenerationInput struct { 48 | TracerCtx context.Context 49 | Tracer trace.Tracer 50 | Name *string `json:"name,omitempty" validate:"required" example:"test"` 51 | } 52 | -------------------------------------------------------------------------------- /examples/machine_fingerprint/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/shirou/gopsutil/cpu" 9 | "github.com/shirou/gopsutil/host" 10 | "go-license-management/internal/utils" 11 | "log" 12 | ) 13 | 14 | type MachineAttribute struct { 15 | CPUModel string `json:"cpu_model"` 16 | Platform string `json:"platform"` 17 | MacAddr []string `json:"mac_addr"` 18 | IPAddr string `json:"ip_addr"` 19 | Serial string `json:"serial"` 20 | } 21 | 22 | func main() { 23 | for _ = range 10 { 24 | // Get host info 25 | hostStat, err := host.Info() 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | // Get cpu stat 31 | cpuStat, err := cpu.Info() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | // Get mac address 37 | macAddress, err := utils.RetrievePhysicalMacAddr() 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | // Get IP address 43 | ip, err := utils.GetOutboundIP() 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | motherboardSerial := hostStat.HostID 48 | cpuModel := cpuStat[0].ModelName 49 | platform := hostStat.Platform 50 | 51 | attr := MachineAttribute{ 52 | CPUModel: cpuModel, 53 | Platform: platform, 54 | MacAddr: macAddress, 55 | IPAddr: ip.String(), 56 | Serial: motherboardSerial, 57 | } 58 | bAttr, err := json.Marshal(attr) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | hash := sha256.Sum256(bAttr) 63 | fingerprint := hex.EncodeToString(hash[:]) 64 | 65 | fmt.Println("fingerprint", fingerprint) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /internal/services/v1/machines/repository/imachine.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/database/entities" 8 | ) 9 | 10 | type IMachine interface { 11 | SelectTenantByName(ctx context.Context, tenantName string) (*entities.Tenant, error) 12 | CheckLicenseExistByPK(ctx context.Context, licenseID uuid.UUID) (bool, error) 13 | CheckMachineExistByFingerprintAndLicense(ctx context.Context, licenseKey, fingerprint string) (bool, error) 14 | SelectLicenseByPK(ctx context.Context, licenseID uuid.UUID) (*entities.License, error) 15 | SelectLicenseByLicenseKey(ctx context.Context, licenseKey string) (*entities.License, error) 16 | SelectMachines(ctx context.Context, tenantName string, queryParam constants.QueryCommonParam) ([]entities.Machine, int, error) 17 | SelectPolicyByPK(ctx context.Context, policyID uuid.UUID) (*entities.Policy, error) 18 | SelectMachineByPK(ctx context.Context, machineID uuid.UUID) (*entities.Machine, error) 19 | InsertNewMachine(ctx context.Context, machine *entities.Machine) error 20 | UpdateMachineByPK(ctx context.Context, machine *entities.Machine) (*entities.Machine, error) 21 | UpdateMachineByPKAndLicense(ctx context.Context, machine *entities.Machine, currentLicense, newLicense *entities.License) (*entities.Machine, error) 22 | InsertNewMachineAndUpdateLicense(ctx context.Context, machine *entities.Machine) error 23 | DeleteMachineByPK(ctx context.Context, machineID uuid.UUID) error 24 | DeleteMachineByPKAndUpdateLicense(ctx context.Context, machineID uuid.UUID) error 25 | } 26 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | local-registry: 5 | image: 'registry:2.7' 6 | container_name: local_registry 7 | ports: 8 | - '5000:5000' 9 | 10 | jaeger: 11 | image: jaegertracing/all-in-one:latest 12 | container_name: backend-tracer 13 | ports: 14 | - "6831:6831" 15 | - "6832:6832/udp" 16 | - "16686:16686" 17 | - "5778:5778" 18 | - "4317:4317" 19 | - "14250:14250" 20 | networks: 21 | - backend 22 | restart: always 23 | 24 | postgres: 25 | container_name: postgres_container 26 | image: postgres 27 | environment: 28 | POSTGRES_USER: postgres 29 | POSTGRES_PASSWORD: postgres 30 | volumes: 31 | - /Users/tripham/docker/postgres:/data/postgres 32 | ports: 33 | - "5432:5432" 34 | networks: 35 | - backend 36 | restart: always 37 | 38 | pgadmin: 39 | container_name: pgadmin_container 40 | image: dpage/pgadmin4 41 | environment: 42 | PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org 43 | PGADMIN_DEFAULT_PASSWORD: admin 44 | volumes: 45 | - /Users/tripham/docker/pgadmin:/var/lib/pgadmin 46 | ports: 47 | - "18080:80" 48 | networks: 49 | - backend 50 | restart: always 51 | 52 | license-manager: 53 | container_name: go-license-manager 54 | image: go-license-manager:latest 55 | volumes: 56 | - ./config.toml:/opt/app/conf/config.toml 57 | ports: 58 | - "8080:8080" 59 | networks: 60 | - backend 61 | depends_on: 62 | - postgres 63 | restart: always 64 | 65 | networks: 66 | backend: 67 | driver: bridge -------------------------------------------------------------------------------- /internal/services/v1/entitlements/models/entitlement_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/models/entitlement_attribute" 8 | "go.opentelemetry.io/otel/trace" 9 | "time" 10 | ) 11 | 12 | type EntitlementRegistrationInput struct { 13 | TracerCtx context.Context 14 | Tracer trace.Tracer 15 | Name *string `json:"name" validate:"required" example:"test"` 16 | Code *string `json:"code" validate:"required" example:"test"` 17 | Metadata map[string]interface{} `json:"metadata" validate:"optional" example:"test"` 18 | entitlement_attribute.EntitlementCommonURI 19 | } 20 | 21 | type EntitlementListInput struct { 22 | TracerCtx context.Context 23 | Tracer trace.Tracer 24 | entitlement_attribute.EntitlementCommonURI 25 | constants.QueryCommonParam 26 | } 27 | 28 | type EntitlementRetrievalInput struct { 29 | TracerCtx context.Context 30 | Tracer trace.Tracer 31 | entitlement_attribute.EntitlementCommonURI 32 | } 33 | 34 | type EntitlementRetrievalOutput struct { 35 | ID uuid.UUID `json:"id"` 36 | TenantName string `json:"tenant_name"` 37 | Name string `json:"name,"` 38 | Code string `json:"code"` 39 | Metadata map[string]interface{} `json:"metadata"` 40 | CreatedAt time.Time `json:"created_at"` 41 | UpdatedAt time.Time `json:"updated_at"` 42 | } 43 | 44 | type EntitlementDeletionInput struct { 45 | TracerCtx context.Context 46 | Tracer trace.Tracer 47 | entitlement_attribute.EntitlementCommonURI 48 | } 49 | -------------------------------------------------------------------------------- /internal/utils/net.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "mime/multipart" 7 | "net" 8 | ) 9 | 10 | func MultipartToBytes(in *multipart.FileHeader) ([]byte, error) { 11 | fInfo, err := in.Open() 12 | if err != nil { 13 | return nil, err 14 | } 15 | defer func() { 16 | cErr := fInfo.Close() 17 | if cErr != nil && err == nil { 18 | err = cErr 19 | } 20 | }() 21 | 22 | content, err := io.ReadAll(fInfo) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return content, err 28 | } 29 | 30 | func RetrievePhysicalMacAddr() ([]string, error) { 31 | interfaces, err := net.Interfaces() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | var as []string 37 | for _, ifa := range interfaces { 38 | if ifa.Flags&net.FlagUp != 0 && bytes.Compare(ifa.HardwareAddr, nil) != 0 { 39 | if ifa.HardwareAddr[0]&2 == 2 { 40 | continue 41 | } 42 | a := ifa.HardwareAddr.String() 43 | if a != "" { 44 | as = append(as, a) 45 | } 46 | } 47 | 48 | } 49 | return as, nil 50 | } 51 | 52 | func RetrieveMacAddr() ([]string, error) { 53 | interfaces, err := net.Interfaces() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | var as []string 59 | for _, ifa := range interfaces { 60 | a := ifa.HardwareAddr.String() 61 | if a != "" { 62 | as = append(as, a) 63 | } 64 | } 65 | return as, nil 66 | } 67 | 68 | func GetOutboundIP() (net.IP, error) { 69 | conn, err := net.Dial("udp", "8.8.8.8:80") 70 | if err != nil { 71 | return nil, err 72 | } 73 | defer func() { 74 | cErr := conn.Close() 75 | if cErr != nil && err == nil { 76 | err = cErr 77 | } 78 | }() 79 | 80 | localAddr := conn.LocalAddr().(*net.UDPAddr) 81 | 82 | return localAddr.IP, nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/services/v1/policies/repository/ipolicy.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/database/entities" 8 | ) 9 | 10 | type IPolicy interface { 11 | InsertNewPolicy(ctx context.Context, policy *entities.Policy) error 12 | InsertNewPolicyEntitlement(ctx context.Context, policyEntitlement *entities.PolicyEntitlement) error 13 | InsertNewPolicyEntitlements(ctx context.Context, policyEntitlement []entities.PolicyEntitlement) error 14 | UpdatePolicyByPK(ctx context.Context, policy *entities.Policy) error 15 | SelectPolicyByPK(ctx context.Context, policyID uuid.UUID) (*entities.Policy, error) 16 | SelectEntitlementByPK(ctx context.Context, entitlementID uuid.UUID) (*entities.Entitlement, error) 17 | SelectEntitlementsByPK(ctx context.Context, entitlementID []uuid.UUID) ([]entities.Entitlement, error) 18 | SelectTenantByName(ctx context.Context, tenantName string) (*entities.Tenant, error) 19 | SelectPolicies(ctx context.Context, tenantName string, queryParam constants.QueryCommonParam) ([]entities.Policy, int, error) 20 | CheckProductExistByID(ctx context.Context, productID uuid.UUID) (bool, error) 21 | CheckPolicyEntitlementExistsByPolicyIDAndEntitlementID(ctx context.Context, policyID, entitlementID uuid.UUID) (bool, error) 22 | DeletePolicyByPK(ctx context.Context, policyID uuid.UUID) error 23 | DeletePolicyEntitlementByPK(ctx context.Context, policyEntitlementID uuid.UUID) error 24 | DeletePolicyEntitlementsByPK(ctx context.Context, policyEntitlementID []uuid.UUID) error 25 | SelectPolicyEntitlements(ctx context.Context, policyID uuid.UUID, queryParam constants.QueryCommonParam) ([]entities.PolicyEntitlement, int, error) 26 | } 27 | -------------------------------------------------------------------------------- /server/api/v1/tokens/token.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go-license-management/internal/middlewares" 6 | "go-license-management/internal/permissions" 7 | "go.opentelemetry.io/otel/trace" 8 | ) 9 | 10 | type TokenRouter struct { 11 | tracer trace.Tracer 12 | } 13 | 14 | func NewTokenRouter() *TokenRouter { 15 | 16 | return &TokenRouter{} 17 | } 18 | 19 | func (r *TokenRouter) Routes(engine *gin.RouterGroup, path string) { 20 | routes := engine.Group(path) 21 | { 22 | routes = routes.Group("/tokens") 23 | routes.POST("", middlewares.JWTValidationMW(), middlewares.PermissionValidationMW(permissions.UserCreate), r.create) 24 | routes.GET("", middlewares.JWTValidationMW(), middlewares.PermissionValidationMW(permissions.UserCreate), r.list) 25 | routes.GET("/:token_id", middlewares.JWTValidationMW(), middlewares.PermissionValidationMW(permissions.UserCreate), r.retrieve) 26 | routes.DELETE("/:token_id", middlewares.JWTValidationMW(), middlewares.PermissionValidationMW(permissions.UserCreate), r.revoke) 27 | routes.PUT("/:token_id", middlewares.JWTValidationMW(), middlewares.PermissionValidationMW(permissions.UserCreate), r.regenerate) 28 | } 29 | } 30 | 31 | // create Generate a new token resource for a user 32 | func (r *TokenRouter) create(ctx *gin.Context) { 33 | 34 | } 35 | 36 | // retrieve retrieves the details of an existing token. 37 | func (r *TokenRouter) retrieve(ctx *gin.Context) { 38 | 39 | } 40 | 41 | // revoke permanently revokes a token. It cannot be undone. 42 | func (r *TokenRouter) revoke(ctx *gin.Context) { 43 | 44 | } 45 | 46 | // list returns a list of tokens. 47 | // The tokens are returned sorted by creation date, with the most recent tokens appearing first. 48 | func (r *TokenRouter) list(ctx *gin.Context) { 49 | 50 | } 51 | 52 | // regenerate regenerates an existing token resource. 53 | func (r *TokenRouter) regenerate(ctx *gin.Context) { 54 | 55 | } 56 | -------------------------------------------------------------------------------- /internal/repositories/v1/authentications/repository.go: -------------------------------------------------------------------------------- 1 | package authentications 2 | 3 | import ( 4 | "context" 5 | "github.com/uptrace/bun" 6 | "go-license-management/internal/cerrors" 7 | "go-license-management/internal/infrastructure/database/entities" 8 | "go-license-management/server/api" 9 | ) 10 | 11 | type AuthenticationRepository struct { 12 | database *bun.DB 13 | } 14 | 15 | func NewAuthenticationRepository(ds *api.DataSource) *AuthenticationRepository { 16 | return &AuthenticationRepository{ 17 | database: ds.GetDatabase(), 18 | } 19 | } 20 | 21 | func (repo *AuthenticationRepository) SelectTenantByPK(ctx context.Context, tenantName string) (*entities.Tenant, error) { 22 | if repo.database == nil { 23 | return nil, cerrors.ErrInvalidDatabaseClient 24 | } 25 | 26 | tenant := &entities.Tenant{Name: tenantName} 27 | 28 | err := repo.database.NewSelect().Model(tenant).ColumnExpr("name, ed25519_private_key").WherePK().Scan(ctx) 29 | if err != nil { 30 | return tenant, err 31 | } 32 | 33 | return tenant, nil 34 | } 35 | 36 | func (repo *AuthenticationRepository) SelectAccountByPK(ctx context.Context, tenantName, username string) (*entities.Account, error) { 37 | if repo.database == nil { 38 | return nil, cerrors.ErrInvalidDatabaseClient 39 | } 40 | 41 | account := &entities.Account{Username: username, TenantName: tenantName} 42 | err := repo.database.NewSelect().Model(account).WherePK().Scan(ctx) 43 | if err != nil { 44 | return account, err 45 | } 46 | return account, nil 47 | } 48 | 49 | func (repo *AuthenticationRepository) SelectMasterByPK(ctx context.Context, username string) (*entities.Master, error) { 50 | if repo.database == nil { 51 | return nil, cerrors.ErrInvalidDatabaseClient 52 | } 53 | 54 | master := &entities.Master{Username: username} 55 | err := repo.database.NewSelect().Model(master).WherePK().Scan(ctx) 56 | if err != nil { 57 | return master, err 58 | } 59 | return master, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/infrastructure/logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "go.elastic.co/ecszap" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "os" 9 | ) 10 | 11 | type Logger struct { 12 | logger *zap.Logger 13 | } 14 | 15 | var currentLogger = &Logger{} 16 | var childLogger *zap.Logger 17 | var logConfig zap.Config 18 | var err error 19 | 20 | func init() { 21 | logConfig = zap.NewProductionConfig() 22 | logConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 23 | logConfig.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 24 | logConfig.EncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder 25 | logConfig.EncoderConfig = ecszap.ECSCompatibleEncoderConfig(logConfig.EncoderConfig) 26 | childLogger, err = logConfig.Build() 27 | if err != nil { 28 | fmt.Printf("failed to initialize logger: %v", err) 29 | os.Exit(1) 30 | } 31 | } 32 | 33 | func NewDefaultLogger() { 34 | newCLogger, err := logConfig.Build() 35 | if err != nil { 36 | fmt.Printf("failed to initialize logger: %v", err) 37 | os.Exit(1) 38 | } 39 | currentLogger.logger = newCLogger 40 | } 41 | 42 | func NewECSLogger() *Logger { 43 | cLogger, err := logConfig.Build() 44 | if err != nil { 45 | fmt.Printf("failed to initialize logger: %v", err) 46 | os.Exit(1) 47 | } 48 | 49 | return &Logger{logger: cLogger} 50 | } 51 | 52 | func GetInstance() *Logger { 53 | return currentLogger 54 | } 55 | 56 | func (l *Logger) GetLogger() *zap.Logger { 57 | return l.logger 58 | } 59 | 60 | func (l *Logger) GetSugarLogger() *zap.SugaredLogger { 61 | return l.logger.Sugar() 62 | } 63 | 64 | func (l *Logger) WithCustomFields(fields ...zap.Field) *zap.Logger { 65 | l.logger = childLogger 66 | l.logger = l.logger.With(fields...) 67 | return l.logger 68 | } 69 | 70 | func (l *Logger) WithCustomStringFields(k string, v string) *zap.Logger { 71 | l.logger = childLogger 72 | l.logger = l.logger.With(zap.String(k, v)) 73 | return l.logger 74 | } 75 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/account.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/uptrace/bun" 5 | "time" 6 | ) 7 | 8 | type Master struct { 9 | bun.BaseModel `bun:"table:masters,alias:ms" swaggerignore:"true"` 10 | 11 | Username string `bun:"username,pk,type:varchar(128)"` 12 | RoleName string `bun:"role_name,type:varchar(256),notnull"` 13 | PasswordDigest string `bun:"password_digest,type:varchar(256)"` 14 | Ed25519PublicKey string `bun:"ed25519_public_key,type:varchar(512),notnull"` 15 | Ed25519PrivateKey string `bun:"ed25519_private_key,type:varchar(512),notnull"` 16 | } 17 | 18 | type Account struct { 19 | bun.BaseModel `bun:"table:accounts,alias:a" swaggerignore:"true"` 20 | 21 | Username string `bun:"username,pk,type:varchar(128)"` 22 | TenantName string `bun:"tenant_name,pk,type:varchar(256),notnull"` 23 | RoleName string `bun:"role_name,type:varchar(256),notnull"` 24 | Email string `bun:"email,type:varchar(256),notnull"` 25 | FirstName string `bun:"first_name,type:varchar(128)"` 26 | LastName string `bun:"last_name,type:varchar(128)"` 27 | Status string `bun:"status,type:varchar(32),notnull"` 28 | PasswordDigest string `bun:"password_digest,type:varchar(256)"` 29 | PasswordResetToken string `bun:"password_reset_token,type:varchar(256),notnull"` 30 | Metadata map[string]interface{} `bun:"metadata,type:jsonb"` 31 | PasswordResetSentAt time.Time `bun:"password_reset_sent_at,nullzero"` 32 | BannedAt time.Time `bun:"banned_at,nullzero"` 33 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 34 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 35 | Tenant *Tenant `bun:"rel:belongs-to,join:tenant_name=name"` 36 | Role *Role `bun:"rel:belongs-to,join:role_name=name"` 37 | } 38 | -------------------------------------------------------------------------------- /internal/permissions/permissions_test.go: -------------------------------------------------------------------------------- 1 | package permissions 2 | 3 | import ( 4 | "github.com/casbin/casbin/v2" 5 | xormadapter "github.com/casbin/xorm-adapter/v3" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestCreateAdminPermission(t *testing.T) { 11 | adminPolicies := CreateAdminPermission() 12 | 13 | a, err := xormadapter.NewAdapter("postgres", 14 | "user=postgres password=123qweA# host=127.0.0.1 port=5432 sslmode=disable") 15 | assert.NoError(t, err) 16 | 17 | e, err := casbin.NewEnforcer("../../conf/rbac_model.conf", a) 18 | assert.NoError(t, err) 19 | 20 | err = e.LoadPolicy() 21 | assert.NoError(t, err) 22 | 23 | // Modify the policy. 24 | for _, record := range adminPolicies { 25 | _, err = e.AddPolicy(record[1], record[2], record[3]) 26 | assert.NoError(t, err) 27 | 28 | err = e.SavePolicy() 29 | assert.NoError(t, err) 30 | } 31 | } 32 | 33 | func TestCreateSuperAdminPermission(t *testing.T) { 34 | superadminPolicies := CreateSuperAdminPermission() 35 | 36 | a, err := xormadapter.NewAdapter("postgres", 37 | "user=postgres password=123qweA# host=127.0.0.1 port=5432 sslmode=disable") 38 | assert.NoError(t, err) 39 | 40 | e, err := casbin.NewEnforcer("../../conf/rbac_model.conf", a) 41 | assert.NoError(t, err) 42 | 43 | err = e.LoadPolicy() 44 | assert.NoError(t, err) 45 | 46 | // Modify the policy. 47 | for _, record := range superadminPolicies { 48 | _, err = e.AddPolicy(record[1], record[2], record[3]) 49 | assert.NoError(t, err) 50 | 51 | err = e.SavePolicy() 52 | assert.NoError(t, err) 53 | } 54 | } 55 | 56 | func TestCreateUserPermission(t *testing.T) { 57 | userPolicies := CreateUserPermission() 58 | 59 | a, err := xormadapter.NewAdapter("postgres", 60 | "user=postgres password=123qweA# host=127.0.0.1 port=5432 sslmode=disable") 61 | assert.NoError(t, err) 62 | 63 | e, err := casbin.NewEnforcer("../../conf/rbac_model.conf", a) 64 | assert.NoError(t, err) 65 | 66 | err = e.LoadPolicy() 67 | assert.NoError(t, err) 68 | 69 | // Modify the policy. 70 | for _, record := range userPolicies { 71 | _, err = e.AddPolicy(record[1], record[2], record[3]) 72 | assert.NoError(t, err) 73 | 74 | err = e.SavePolicy() 75 | assert.NoError(t, err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/utils/encrypt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "errors" 9 | "golang.org/x/crypto/bcrypt" 10 | "io" 11 | ) 12 | 13 | // Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of 14 | // the data and provides a check that it hasn't been altered. Output takes the 15 | // form nonce|ciphertext|tag where '|' indicates concatenation. 16 | func Encrypt(plaintext []byte, key []byte) (ciphertext []byte, err error) { 17 | k := sha256.Sum256(key) 18 | block, err := aes.NewCipher(k[:]) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | gcm, err := cipher.NewGCM(block) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | nonce := make([]byte, gcm.NonceSize()) 29 | _, err = io.ReadFull(rand.Reader, nonce) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return gcm.Seal(nonce, nonce, plaintext, nil), nil 35 | } 36 | 37 | // Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of 38 | // the data and provides a check that it hasn't been altered. Expects input 39 | // form nonce|ciphertext|tag where '|' indicates concatenation. 40 | func Decrypt(ciphertext []byte, key []byte) (plaintext []byte, err error) { 41 | k := sha256.Sum256(key) 42 | block, err := aes.NewCipher(k[:]) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | gcm, err := cipher.NewGCM(block) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | if len(ciphertext) < gcm.NonceSize() { 53 | return nil, errors.New("malformed ciphertext") 54 | } 55 | 56 | return gcm.Open(nil, 57 | ciphertext[:gcm.NonceSize()], 58 | ciphertext[gcm.NonceSize():], 59 | nil, 60 | ) 61 | } 62 | 63 | // HashPassword hashes the password using default cost 64 | func HashPassword(password string) (string, error) { 65 | bPassword := []byte(password) 66 | hashedPassword, err := bcrypt.GenerateFromPassword(bPassword, bcrypt.DefaultCost) 67 | if err != nil { 68 | return "", err 69 | } 70 | return string(hashedPassword), nil 71 | 72 | } 73 | 74 | // CompareHashedPassword compares input plaintext with its possible hash value 75 | func CompareHashedPassword(hashedPassword, currPassword string) bool { 76 | err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(currPassword)) 77 | return err == nil 78 | } 79 | -------------------------------------------------------------------------------- /server/api/root_router.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go-license-management/server/api/v1/accounts" 6 | "go-license-management/server/api/v1/authentications" 7 | "go-license-management/server/api/v1/entitlements" 8 | "go-license-management/server/api/v1/licenses" 9 | "go-license-management/server/api/v1/machines" 10 | "go-license-management/server/api/v1/policies" 11 | "go-license-management/server/api/v1/products" 12 | "go-license-management/server/api/v1/tenants" 13 | ) 14 | 15 | type RootRouter struct { 16 | AppService *AppService 17 | } 18 | 19 | func New(appService *AppService) *RootRouter { 20 | return &RootRouter{ 21 | AppService: appService, 22 | } 23 | } 24 | 25 | func (rr *RootRouter) InitRouters(engine *gin.Engine) { 26 | // root 27 | rootRouter := engine.Group("/api") 28 | { 29 | v1Router := rootRouter.Group("/v1") 30 | 31 | // tenant route 32 | tenantRoute := tenants.NewTenantRouter(rr.AppService.GetV1Svc().GetTenant()) 33 | tenantRoute.Routes(v1Router, "") 34 | 35 | superAdminRoute := authentications.NewAuthenticationRouter(rr.AppService.GetV1Svc().GetAuth()) 36 | superAdminRoute.Routes(v1Router, "") 37 | 38 | // common path prefix 39 | prefix := "tenants/:tenant_name" 40 | 41 | // Authentication routes 42 | authenRoute := authentications.NewAuthenticationRouter(rr.AppService.GetV1Svc().GetAuth()) 43 | authenRoute.Routes(v1Router, prefix) 44 | 45 | // Account routes 46 | accountRoute := accounts.NewAccountRouter(rr.AppService.GetV1Svc().GetAccount()) 47 | accountRoute.Routes(v1Router, prefix) 48 | 49 | // Product routes 50 | productRoute := products.NewProductRouter(rr.AppService.GetV1Svc().GetProduct()) 51 | productRoute.Routes(v1Router, prefix) 52 | 53 | // Entitlement routes 54 | entitlementRoute := entitlements.NewEntitlementRouter(rr.AppService.GetV1Svc().GetEntitlement()) 55 | entitlementRoute.Routes(v1Router, prefix) 56 | 57 | // Policy routes 58 | policyRoute := policies.NewPolicyRouter(rr.AppService.GetV1Svc().GetPolicy()) 59 | policyRoute.Routes(v1Router, prefix) 60 | 61 | // License routes 62 | licenseRoute := licenses.NewLicenseRouter(rr.AppService.GetV1Svc().GetLicense()) 63 | licenseRoute.Routes(v1Router, prefix) 64 | 65 | // Machine routes 66 | machineRoute := machines.NewMachineRouter(rr.AppService.GetV1Svc().GetMachine()) 67 | machineRoute.Routes(v1Router, prefix) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /internal/infrastructure/tracer/tracer.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "context" 5 | "go.opentelemetry.io/otel" 6 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 7 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 8 | "go.opentelemetry.io/otel/propagation" 9 | "go.opentelemetry.io/otel/sdk/resource" 10 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 11 | semconv "go.opentelemetry.io/otel/semconv/v1.26.0" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials/insecure" 14 | "google.golang.org/grpc/keepalive" 15 | 16 | "time" 17 | ) 18 | 19 | var tp *sdktrace.TracerProvider 20 | var tExp *otlptrace.Exporter 21 | 22 | func GetInstance() *sdktrace.TracerProvider { 23 | return tp 24 | } 25 | 26 | func NewTracerProvider(grpcHost string, serviceName, namespace string) error { 27 | var err error 28 | timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 29 | defer cancel() 30 | 31 | clientConn, err := grpc.NewClient(grpcHost, grpc.WithTransportCredentials(insecure.NewCredentials())) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | traceClient := otlptracegrpc.NewClient( 37 | otlptracegrpc.WithGRPCConn(clientConn), 38 | otlptracegrpc.WithInsecure(), 39 | otlptracegrpc.WithDialOption(grpc.WithKeepaliveParams(keepalive.ClientParameters{PermitWithoutStream: true})), 40 | ) 41 | 42 | tExp, err = otlptrace.New(timeoutCtx, traceClient) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | res, err := resource.New(timeoutCtx, 48 | resource.WithFromEnv(), 49 | resource.WithProcess(), 50 | resource.WithTelemetrySDK(), 51 | resource.WithHost(), 52 | resource.WithAttributes( 53 | semconv.ServiceName(serviceName), 54 | semconv.K8SNamespaceName(namespace), 55 | ), 56 | ) 57 | if err != nil { 58 | return err 59 | } 60 | bsp := sdktrace.NewBatchSpanProcessor(tExp) 61 | tp = sdktrace.NewTracerProvider( 62 | sdktrace.WithSampler(sdktrace.AlwaysSample()), 63 | sdktrace.WithResource(res), 64 | sdktrace.WithSpanProcessor(bsp), 65 | ) 66 | 67 | otel.SetTextMapPropagator(propagation.TraceContext{}) 68 | otel.SetTracerProvider(tp) 69 | return nil 70 | } 71 | 72 | func Shutdown() { 73 | if tExp == nil { 74 | return 75 | } 76 | ctx := context.Background() 77 | cCtx, cancel := context.WithTimeout(ctx, time.Second) 78 | defer cancel() 79 | err := tExp.Shutdown(cCtx) 80 | if err != nil { 81 | otel.Handle(err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/constants/license.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // DefaultLicenseTTL approximately 1 month 5 | DefaultLicenseTTL = 2629746 6 | MinimumLicenseTTL = 3600 7 | // MaximumLicenseTTL approximately 1 year 8 | MaximumLicenseTTL = 31556952 9 | ) 10 | 11 | const ( 12 | LicenseActionValidate = "validate" 13 | LicenseActionSuspend = "suspend" 14 | LicenseActionReinstate = "reinstate" 15 | LicenseActionRenew = "renew" 16 | LicenseActionCheckout = "checkout" 17 | LicenseActionCheckin = "checkin" 18 | LicenseActionIncrementUsage = "increment-usage" 19 | LicenseActionDecrementUsage = "decrement-usage" 20 | LicenseActionResetUsage = "reset-usage" 21 | ) 22 | 23 | var ValidLicenseActionMapper = map[string]interface{}{ 24 | LicenseActionValidate: true, 25 | LicenseActionSuspend: true, 26 | LicenseActionReinstate: true, 27 | LicenseActionRenew: true, 28 | LicenseActionCheckout: true, 29 | LicenseActionCheckin: true, 30 | LicenseActionIncrementUsage: true, 31 | LicenseActionDecrementUsage: true, 32 | LicenseActionResetUsage: true, 33 | } 34 | 35 | //The status of the license to filter by. One of: ACTIVE, INACTIVE, EXPIRED, SUSPENDED, or BANNED. 36 | 37 | const ( 38 | LicenseStatusNotActivated = "not_activated" 39 | LicenseStatusActive = "active" 40 | LicenseStatusInactive = "inactive" 41 | LicenseStatusSuspended = "suspended" 42 | LicenseStatusExpired = "expired" 43 | LicenseStatusBanned = "banned" 44 | ) 45 | 46 | const ( 47 | LicenseValidationStatusValid = "valid" // The validated license resource or license key is valid. 48 | LicenseValidationStatusSuspended = "suspended" // The validated license has been suspended. 49 | LicenseValidationStatusExpired = "expired" // The validated license is expired. 50 | LicenseValidationStatusBanned = "banned" // The user that owns the validated license has been banned. 51 | LicenseValidationStatusOverdue = "overdue" // The validated license is overdue for check-in. 52 | LicenseValidationStatusNoMachine = "no_machine" // Not activated. The validated license does not meet its node-locked policy's requirement of exactly 1 associated machine. 53 | LicenseValidationStatusTooManyMachine = "too_many_machines" // The validated license has exceeded its policy's machine limit. 54 | ) 55 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/license.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/uptrace/bun" 6 | "time" 7 | ) 8 | 9 | type License struct { 10 | bun.BaseModel `bun:"table:licenses,alias:l" swaggerignore:"true"` 11 | 12 | ID uuid.UUID `bun:"id,pk,type:uuid"` 13 | PolicyID uuid.UUID `bun:"policy_id,type:uuid,notnull"` 14 | ProductID uuid.UUID `bun:"product_id,type:uuid,notnull"` 15 | TenantName string `bun:"tenant_name,type:varchar(256),notnull"` 16 | Key string `bun:"key,type:varchar(256),notnull"` 17 | Name string `bun:"name,type:varchar(256),notnull"` 18 | LastValidatedChecksum string `bun:"last_validated_checksum,type:varchar(1028),notnull"` 19 | Status string `bun:"status,type:varchar(64),notnull"` 20 | Suspended bool `bun:"suspended,default:false"` 21 | Uses int `bun:"uses,type:integer,default:0"` 22 | MachinesCount int `bun:"machines_count,type:integer,default:0"` 23 | Users int `bun:"users,default:0,notnull"` 24 | MaxMachines int `bun:"max_machines"` 25 | MaxUses int `bun:"max_uses"` 26 | MaxUsers int `bun:"max_users"` 27 | Metadata map[string]interface{} `bun:"metadata,type:jsonb"` 28 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 29 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 30 | Expiry time.Time `bun:"expiry,nullzero"` 31 | LastCheckInAt time.Time `bun:"last_checked_in_at,nullzero"` 32 | LastExpirationEventSentAt time.Time `bun:"last_expiration_event_sent_at,nullzero"` 33 | LastCheckInEventSentAt time.Time `bun:"last_checked_in_event_sent_at,nullzero"` 34 | LastCheckOutAt time.Time `bun:"last_checkout_at,nullzero"` 35 | LastValidatedAt time.Time `bun:"last_validated_at,nullzero"` 36 | Tenant *Tenant `bun:"rel:belongs-to,join:tenant_name=name"` 37 | Product *Product `bun:"rel:belongs-to,join:product_id=id"` 38 | Policy *Policy `bun:"rel:belongs-to,join:policy_id=id"` 39 | } 40 | -------------------------------------------------------------------------------- /server/api/v1/service.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | accountSvc "go-license-management/internal/services/v1/accounts/service" 5 | authSvc "go-license-management/internal/services/v1/authentications/service" 6 | entitlementSvc "go-license-management/internal/services/v1/entitlements/service" 7 | licenseSvc "go-license-management/internal/services/v1/licenses/service" 8 | machineSvc "go-license-management/internal/services/v1/machines/service" 9 | policySvc "go-license-management/internal/services/v1/policies/service" 10 | productSvc "go-license-management/internal/services/v1/products/service" 11 | tenantSvc "go-license-management/internal/services/v1/tenants/service" 12 | ) 13 | 14 | type V1AppService struct { 15 | account *accountSvc.AccountService 16 | tenant *tenantSvc.TenantService 17 | product *productSvc.ProductService 18 | policy *policySvc.PolicyService 19 | entitlement *entitlementSvc.EntitlementService 20 | machine *machineSvc.MachineService 21 | authentication *authSvc.AuthenticationService 22 | license *licenseSvc.LicenseService 23 | } 24 | 25 | func (v1 *V1AppService) GetAccount() *accountSvc.AccountService { 26 | return v1.account 27 | } 28 | 29 | func (v1 *V1AppService) SetAccount(svc *accountSvc.AccountService) { 30 | v1.account = svc 31 | } 32 | 33 | func (v1 *V1AppService) GetTenant() *tenantSvc.TenantService { 34 | return v1.tenant 35 | } 36 | 37 | func (v1 *V1AppService) SetTenant(svc *tenantSvc.TenantService) { 38 | v1.tenant = svc 39 | } 40 | 41 | func (v1 *V1AppService) GetProduct() *productSvc.ProductService { 42 | return v1.product 43 | } 44 | 45 | func (v1 *V1AppService) SetProduct(svc *productSvc.ProductService) { 46 | v1.product = svc 47 | } 48 | 49 | func (v1 *V1AppService) GetPolicy() *policySvc.PolicyService { 50 | return v1.policy 51 | } 52 | 53 | func (v1 *V1AppService) SetPolicy(svc *policySvc.PolicyService) { 54 | v1.policy = svc 55 | } 56 | 57 | func (v1 *V1AppService) GetEntitlement() *entitlementSvc.EntitlementService { 58 | return v1.entitlement 59 | } 60 | 61 | func (v1 *V1AppService) SetEntitlement(svc *entitlementSvc.EntitlementService) { 62 | v1.entitlement = svc 63 | } 64 | 65 | func (v1 *V1AppService) GetMachine() *machineSvc.MachineService { 66 | return v1.machine 67 | } 68 | 69 | func (v1 *V1AppService) SetMachine(svc *machineSvc.MachineService) { 70 | v1.machine = svc 71 | } 72 | 73 | func (v1 *V1AppService) GetAuth() *authSvc.AuthenticationService { 74 | return v1.authentication 75 | } 76 | 77 | func (v1 *V1AppService) SetAuth(svc *authSvc.AuthenticationService) { 78 | v1.authentication = svc 79 | } 80 | 81 | func (v1 *V1AppService) GetLicense() *licenseSvc.LicenseService { 82 | return v1.license 83 | } 84 | 85 | func (v1 *V1AppService) SetLicense(svc *licenseSvc.LicenseService) { 86 | v1.license = svc 87 | } 88 | -------------------------------------------------------------------------------- /internal/infrastructure/casbin_adapter/xorm_adapter.go: -------------------------------------------------------------------------------- 1 | package casbin_adapter 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/casbin/casbin/v2" 7 | "github.com/casbin/casbin/v2/model" 8 | xormadapter "github.com/casbin/xorm-adapter/v3" 9 | _ "github.com/lib/pq" 10 | "go-license-management/internal/infrastructure/logging" 11 | "go-license-management/internal/permissions" 12 | ) 13 | 14 | func init() { 15 | enforcerModel = model.NewModel() 16 | enforcerModel.AddDef("r", "r", "dom, sub, obj, act") 17 | enforcerModel.AddDef("p", "p", "sub, obj, act") 18 | enforcerModel.AddDef("g", "g", "_, _, _") 19 | enforcerModel.AddDef("e", "e", "some(where (p.eft == allow)) && !some(where (p.eft == deny))") 20 | enforcerModel.AddDef("m", "m", "g(r.dom, r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == \"superadmin\"") 21 | } 22 | 23 | var enforcerModel model.Model 24 | 25 | func GetEnforcerModel() model.Model { 26 | return enforcerModel 27 | } 28 | 29 | var adapter *xormadapter.Adapter 30 | 31 | func GetAdapter() *xormadapter.Adapter { 32 | return adapter 33 | } 34 | 35 | func NewCasbinAdapter(userName, password, host, port string) (*xormadapter.Adapter, error) { 36 | var err error 37 | 38 | if host == "" || userName == "" || password == "" || port == "" { 39 | return nil, errors.New("one or more required connection parameters are empty") 40 | } 41 | 42 | adapter, err = xormadapter.NewAdapter( 43 | "postgres", 44 | fmt.Sprintf("user=%s password=%s host=%s port=%s sslmode=disable", 45 | userName, password, host, port, 46 | ), 47 | ) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return adapter, nil 52 | } 53 | 54 | func SeedingCasbinPermissions() error { 55 | logging.GetInstance().GetLogger().Info("started populating casbin data") 56 | superadminPolicies := permissions.CreateSuperAdminPermission() 57 | adminPolicies := permissions.CreateAdminPermission() 58 | userPolicies := permissions.CreateUserPermission() 59 | 60 | e, err := casbin.NewEnforcer(GetEnforcerModel(), GetAdapter()) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | err = e.LoadPolicy() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Modify the policy. 71 | policies := make([][]string, 0) 72 | for _, record := range superadminPolicies { 73 | policies = append(policies, []string{record[1], record[2], record[3]}) 74 | } 75 | for _, record := range adminPolicies { 76 | policies = append(policies, []string{record[1], record[2], record[3]}) 77 | } 78 | for _, record := range userPolicies { 79 | policies = append(policies, []string{record[1], record[2], record[3]}) 80 | } 81 | 82 | _, err = e.AddPolicies(policies) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | err = e.LoadPolicy() 88 | if err != nil { 89 | return err 90 | } 91 | logging.GetInstance().GetLogger().Info("completed populating casbin data") 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/services/v1/policies/models/policy_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/constants" 6 | "go-license-management/internal/infrastructure/models/policy_attribute" 7 | "go.opentelemetry.io/otel/trace" 8 | "time" 9 | ) 10 | 11 | type PolicyRegistrationInput struct { 12 | TracerCtx context.Context 13 | Tracer trace.Tracer 14 | policy_attribute.PolicyCommonURI 15 | policy_attribute.PolicyAttributeModel 16 | ProductID *string `json:"product_id" validate:"required" example:"test"` 17 | } 18 | 19 | type PolicyListInput struct { 20 | TracerCtx context.Context 21 | Tracer trace.Tracer 22 | policy_attribute.PolicyCommonURI 23 | constants.QueryCommonParam 24 | } 25 | 26 | type PolicyRetrievalInput struct { 27 | TracerCtx context.Context 28 | Tracer trace.Tracer 29 | policy_attribute.PolicyCommonURI 30 | } 31 | 32 | type PolicyRetrievalOutput struct { 33 | ID string `json:"id"` 34 | TenantName string `json:"tenant_name"` 35 | PublicKey string `json:"public_key"` 36 | CreatedAt time.Time `json:"created_at"` 37 | UpdatedAt time.Time `json:"updated_at"` 38 | policy_attribute.PolicyAttributeModel 39 | } 40 | 41 | type PolicyUpdateInput struct { 42 | TracerCtx context.Context 43 | Tracer trace.Tracer 44 | policy_attribute.PolicyAttributeModel 45 | policy_attribute.PolicyCommonURI 46 | } 47 | 48 | type PolicyDeletionInput struct { 49 | TracerCtx context.Context 50 | Tracer trace.Tracer 51 | policy_attribute.PolicyCommonURI 52 | } 53 | 54 | type PolicyAttachmentInput struct { 55 | TracerCtx context.Context 56 | Tracer trace.Tracer 57 | policy_attribute.PolicyCommonURI 58 | EntitlementID []string `json:"entitlement_id"` 59 | } 60 | 61 | type PolicyAttachmentOutput struct { 62 | ID string `json:"id"` 63 | TenantName string `json:"tenant_name"` 64 | PolicyID string `json:"policy_id"` 65 | EntitlementID string `json:"entitlement_id"` 66 | Metadata map[string]interface{} `json:"metadata"` 67 | CreatedAt time.Time `json:"created_at"` 68 | UpdatedAt time.Time `json:"updated_at"` 69 | } 70 | 71 | type PolicyDetachmentInput struct { 72 | TracerCtx context.Context 73 | Tracer trace.Tracer 74 | policy_attribute.PolicyCommonURI 75 | ID []string `json:"id"` 76 | } 77 | 78 | type PolicyEntitlementListInput struct { 79 | TracerCtx context.Context 80 | Tracer trace.Tracer 81 | policy_attribute.PolicyCommonURI 82 | constants.QueryCommonParam 83 | } 84 | 85 | type PolicyEntitlementListOutput struct { 86 | ID string `json:"id"` 87 | TenantName string `json:"tenant_name"` 88 | PolicyID string `json:"policy_id"` 89 | EntitlementID string `json:"entitlement_id"` 90 | Metadata map[string]interface{} `json:"metadata"` 91 | CreatedAt time.Time `json:"created_at"` 92 | UpdatedAt time.Time `json:"updated_at"` 93 | } 94 | -------------------------------------------------------------------------------- /server/api/v1/tenants/tenant_model.go: -------------------------------------------------------------------------------- 1 | package tenants 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/services/v1/tenants/models" 8 | "go.opentelemetry.io/otel/trace" 9 | ) 10 | 11 | type TenantRegistrationRequest struct { 12 | Name *string `form:"name" validate:"required" example:"test"` 13 | } 14 | 15 | func (req *TenantRegistrationRequest) Validate() error { 16 | if req.Name == nil { 17 | return cerrors.ErrTenantNameIsEmpty 18 | } 19 | 20 | return nil 21 | } 22 | 23 | func (req *TenantRegistrationRequest) ToTenantRegistrationInput(ctx context.Context, tracer trace.Tracer) *models.TenantRegistrationInput { 24 | return &models.TenantRegistrationInput{ 25 | TracerCtx: ctx, 26 | Tracer: tracer, 27 | Name: req.Name, 28 | } 29 | } 30 | 31 | type TenantRetrievalRequest struct { 32 | TenantName *string `uri:"tenant_name" binding:"required"` 33 | } 34 | 35 | func (req *TenantRetrievalRequest) Validate() error { 36 | if req.TenantName == nil { 37 | return cerrors.ErrTenantNameIsEmpty 38 | } 39 | return nil 40 | } 41 | 42 | func (req *TenantRetrievalRequest) ToTenantRetrievalInput(ctx context.Context, tracer trace.Tracer) *models.TenantRetrievalInput { 43 | return &models.TenantRetrievalInput{ 44 | TracerCtx: ctx, 45 | Tracer: tracer, 46 | Name: req.TenantName, 47 | } 48 | } 49 | 50 | type TenantDeletionRequest struct { 51 | TenantName *string `uri:"tenant_name" binding:"required"` 52 | } 53 | 54 | func (req *TenantDeletionRequest) Validate() error { 55 | if req.TenantName == nil { 56 | return cerrors.ErrTenantNameIsEmpty 57 | } 58 | return nil 59 | } 60 | 61 | func (req *TenantDeletionRequest) ToTenantDeletionInput(ctx context.Context, tracer trace.Tracer) *models.TenantDeletionInput { 62 | return &models.TenantDeletionInput{ 63 | TracerCtx: ctx, 64 | Tracer: tracer, 65 | Name: req.TenantName, 66 | } 67 | } 68 | 69 | type TenantListRequest struct { 70 | constants.QueryCommonParam 71 | } 72 | 73 | func (req *TenantListRequest) Validate() error { 74 | req.QueryCommonParam.Validate() 75 | return nil 76 | } 77 | 78 | func (req *TenantListRequest) ToTenantListInput(ctx context.Context, tracer trace.Tracer) *models.TenantListInput { 79 | return &models.TenantListInput{ 80 | TracerCtx: ctx, 81 | Tracer: tracer, 82 | QueryCommonParam: req.QueryCommonParam, 83 | } 84 | } 85 | 86 | type TenantRegenerationRequest struct { 87 | TenantName *string `uri:"tenant_name" binding:"required"` 88 | } 89 | 90 | func (req *TenantRegenerationRequest) Validate() error { 91 | if req.TenantName == nil { 92 | return cerrors.ErrTenantNameIsEmpty 93 | } 94 | return nil 95 | } 96 | 97 | func (req *TenantRegenerationRequest) ToTenantRegenerationInput(ctx context.Context, tracer trace.Tracer) *models.TenantRegenerationInput { 98 | return &models.TenantRegenerationInput{ 99 | TracerCtx: ctx, 100 | Tracer: tracer, 101 | Name: req.TenantName, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/gin-contrib/cors" 8 | "github.com/gin-contrib/gzip" 9 | "github.com/gin-gonic/gin" 10 | "github.com/spf13/viper" 11 | swaggerFiles "github.com/swaggo/files" 12 | ginSwagger "github.com/swaggo/gin-swagger" 13 | _ "go-license-management/docs" 14 | "go-license-management/internal/config" 15 | "go-license-management/internal/constants" 16 | "go-license-management/internal/infrastructure/logging" 17 | "go-license-management/internal/middlewares" 18 | "go-license-management/server/api" 19 | "net/http" 20 | "os" 21 | "time" 22 | ) 23 | 24 | // StartServer starts the API server 25 | func StartServer(appService *api.AppService, quit chan os.Signal) { 26 | gin.SetMode(viper.GetString(config.ServerMode)) 27 | router := gin.New() 28 | router.Use(gin.Recovery()) 29 | 30 | router.Use(cors.New(cors.Config{ 31 | AllowOrigins: []string{constants.AllowAllOrigins}, 32 | AllowMethods: []string{http.MethodPost, http.MethodPatch, http.MethodPut, http.MethodGet, http.MethodDelete}, 33 | AllowHeaders: []string{constants.AccessControlAllowHeadersHeader, constants.OriginHeader, constants.AcceptHeader, 34 | constants.XRequestedWithHeader, constants.ContentTypeHeader, constants.AuthorizationHeader, constants.XAPIKeyHeader}, 35 | ExposeHeaders: []string{constants.ContentLengthHeader}, 36 | AllowCredentials: true, 37 | })) 38 | 39 | router.Use( 40 | middlewares.RequestIDMW(), middlewares.TimeoutMW(), gzip.Gzip(gzip.DefaultCompression), 41 | middlewares.Recovery(), middlewares.LoggerMW(logging.GetInstance().GetLogger()), middlewares.HashHeaderMW(), 42 | ) 43 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 44 | 45 | rootRouter := api.New(appService) 46 | rootRouter.InitRouters(router) 47 | 48 | serverPort := "8888" 49 | if viper.GetString(config.ServerHttpPort) != "" { 50 | serverPort = viper.GetString(config.ServerHttpPort) 51 | } 52 | 53 | serverAddr := fmt.Sprintf("0.0.0.0:%s", serverPort) 54 | srv := &http.Server{ 55 | Addr: serverAddr, 56 | Handler: router, 57 | } 58 | 59 | go func() { 60 | var err error 61 | 62 | if viper.GetBool(config.ServerEnableTLS) { 63 | logging.GetInstance().GetLogger().Info("tls enabled") 64 | err = srv.ListenAndServeTLS(viper.GetString(config.ServerCertFile), viper.GetString(config.ServerKeyFile)) 65 | } else { 66 | logging.GetInstance().GetLogger().Info("tls disabled") 67 | err = srv.ListenAndServe() 68 | } 69 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 70 | logging.GetInstance().GetLogger().Error(err.Error()) 71 | } 72 | }() 73 | logging.GetInstance().GetLogger().Info(fmt.Sprintf("startup completed at: %s", serverAddr)) 74 | 75 | <-quit 76 | 77 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 78 | defer cancel() 79 | if err := srv.Shutdown(ctx); err != nil { 80 | logging.GetInstance().GetLogger().Error(fmt.Sprintf("error shutting down server: %s", err.Error())) 81 | } 82 | 83 | select { 84 | case <-ctx.Done(): 85 | logging.GetInstance().GetLogger().Info("server shutdown completed") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/constants/http.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import "go-license-management/internal/utils" 4 | 5 | const ( 6 | ContentDispositionInline = "inline" 7 | ContentDispositionAttachment = "attachment; filename=%s" 8 | ) 9 | const ( 10 | ContentTypeBinary = "application/octet-stream" 11 | ContentTypeForm = "application/x-www-form-urlencoded" 12 | ContentTypeJSON = "application/json" 13 | ContentTypeHTML = "text/html; charset=utf-8" 14 | ContentTypeText = "text/plain; charset=utf-8" 15 | ContentTypeImage = "image/%s" 16 | ContentTypeXML = "text/xml" 17 | ContentTypePDF = "application/pdf" 18 | ) 19 | 20 | const ( 21 | MimeTypeJPEG = "image/jpeg" 22 | MimeTypePNG = "image/png" 23 | MimeTypePlainText = "text/plain" 24 | MimeTypeZip = "application/zip" 25 | MimeTypeRar = "application/x-rar-compressed" 26 | MimeTypeXML = "application/xml" 27 | ) 28 | 29 | const ( 30 | AllowAllOrigins = "*" 31 | ) 32 | 33 | const ( 34 | AcceptHeader = "Accept" 35 | AcceptLanguageHeader = "Accept-Language" 36 | AccessControlAllowHeadersHeader = "Access-Control-Allow-Headers" 37 | AuthorizationHeader = "Authorization" 38 | ContentLengthHeader = "Content-Length" 39 | ContentTypeHeader = "Content-Type" 40 | ContentDispositionHeader = "Content-Disposition" 41 | ContentDigestHeader = "Content-Digest" 42 | ContentTransferEncodingHeader = "Content-Transfer-Encoding" 43 | ContentDescriptionHeader = "Content-Description" 44 | OriginHeader = "Origin" 45 | XRequestIDHeader = "X-Request-ID" 46 | XRequestedWithHeader = "X-Requested-With" 47 | XAPIKeyHeader = "X-API-Key" 48 | XRateLimitWindowHeader = "X-RateLimit-Window" // The current rate limiting window that is closest to being reached, percentage-wise. 49 | XRateLimitCountHeader = "X-RateLimit-Count" // The number of requests that have been performed within the current rate limit window. 50 | XRateLimitLimitHeader = "X-RateLimit-Limit" // The maximum number of requests that the IP is permitted to make for the current window. 51 | RetryAfterHeader = "Retry-After" 52 | XRateLimitRemainingHeader = "X-RateLimit-Remaining" // The number of requests remaining in the current rate limit window. 53 | XRateLimitResetHeader = "X-RateLimit-Reset" // The time at which the current rate limit window resets in UTC epoch seconds. 54 | XLicenseChecksumHeader = "X-License-Checksum" 55 | XMachineChecksumHeader = "X-Machine-Checksum" 56 | ) 57 | 58 | const ( 59 | AuthorizationTypeBearer = "Bearer" 60 | ) 61 | 62 | const ( 63 | ContextValuePermissions = "permissions" 64 | ContextValueTenant = "tenant" 65 | ContextValueSubject = "subject" 66 | ContextValueAudience = "audience" 67 | ) 68 | 69 | type QueryCommonParam struct { 70 | Limit *int `form:"limit" validate:"optional" example:"10"` 71 | Offset *int `form:"offset" validate:"optional" example:"10"` 72 | } 73 | 74 | func (req *QueryCommonParam) Validate() { 75 | if req.Limit == nil { 76 | req.Limit = utils.RefPointer(100) 77 | } 78 | 79 | if req.Offset == nil { 80 | req.Offset = utils.RefPointer(0) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/repositories/v1/tenants/repository.go: -------------------------------------------------------------------------------- 1 | package tenants 2 | 3 | import ( 4 | "context" 5 | "github.com/uptrace/bun" 6 | "go-license-management/internal/cerrors" 7 | "go-license-management/internal/constants" 8 | "go-license-management/internal/infrastructure/database/entities" 9 | "go-license-management/internal/utils" 10 | "go-license-management/server/api" 11 | "time" 12 | ) 13 | 14 | type TenantRepository struct { 15 | database *bun.DB 16 | } 17 | 18 | func NewTenantRepository(ds *api.DataSource) *TenantRepository { 19 | return &TenantRepository{ 20 | database: ds.GetDatabase(), 21 | } 22 | } 23 | 24 | func (repo *TenantRepository) SelectTenants(ctx context.Context, queryParam constants.QueryCommonParam) ([]entities.Tenant, int, error) { 25 | var count = 0 26 | 27 | if repo.database == nil { 28 | return nil, count, cerrors.ErrInvalidDatabaseClient 29 | } 30 | 31 | tenant := make([]entities.Tenant, 0) 32 | count, err := repo.database.NewSelect().Model(new(entities.Tenant)). 33 | Order("created_at DESC"). 34 | Limit(utils.DerefPointer(queryParam.Limit)). 35 | Offset(utils.DerefPointer(queryParam.Offset)). 36 | ScanAndCount(ctx, &tenant) 37 | if err != nil { 38 | return tenant, count, err 39 | } 40 | return tenant, count, nil 41 | } 42 | 43 | func (repo *TenantRepository) SelectTenantByPK(ctx context.Context, name string) (*entities.Tenant, error) { 44 | if repo.database == nil { 45 | return nil, cerrors.ErrInvalidDatabaseClient 46 | } 47 | 48 | tenant := &entities.Tenant{ 49 | Name: name, 50 | } 51 | err := repo.database.NewSelect().Model(tenant).WherePK().Scan(ctx) 52 | if err != nil { 53 | return tenant, err 54 | } 55 | return tenant, nil 56 | } 57 | 58 | func (repo *TenantRepository) CheckTenantExistByPK(ctx context.Context, name string) (bool, error) { 59 | if repo.database == nil { 60 | return false, cerrors.ErrInvalidDatabaseClient 61 | } 62 | 63 | tenant := &entities.Tenant{Name: name} 64 | 65 | exist, err := repo.database.NewSelect().Model(tenant).WherePK().Exists(ctx) 66 | if err != nil { 67 | return exist, err 68 | } 69 | return exist, nil 70 | } 71 | 72 | func (repo *TenantRepository) InsertNewTenant(ctx context.Context, tenant *entities.Tenant) error { 73 | if repo.database == nil { 74 | return cerrors.ErrInvalidDatabaseClient 75 | } 76 | 77 | _, err := repo.database.NewInsert().Model(tenant).Exec(ctx) 78 | if err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | 84 | func (repo *TenantRepository) DeleteTenantByPK(ctx context.Context, name string) error { 85 | if repo.database == nil { 86 | return cerrors.ErrInvalidDatabaseClient 87 | } 88 | 89 | tenant := &entities.Tenant{Name: name} 90 | 91 | _, err := repo.database.NewDelete().Model(tenant).WherePK().Exec(ctx) 92 | if err != nil { 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | func (repo *TenantRepository) UpdateTenantByPK(ctx context.Context, tenant *entities.Tenant) (*entities.Tenant, error) { 99 | if repo.database == nil { 100 | return tenant, cerrors.ErrInvalidDatabaseClient 101 | } 102 | 103 | tenant.UpdatedAt = time.Now() 104 | _, err := repo.database.NewUpdate().Model(tenant).WherePK().Exec(ctx) 105 | if err != nil { 106 | return tenant, err 107 | } 108 | return tenant, nil 109 | } 110 | -------------------------------------------------------------------------------- /server/api/v1/entitlements/entitlement_model.go: -------------------------------------------------------------------------------- 1 | package entitlements 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/models/entitlement_attribute" 8 | "go-license-management/internal/services/v1/entitlements/models" 9 | "go.opentelemetry.io/otel/trace" 10 | ) 11 | 12 | type EntitlementRegistrationRequest struct { 13 | Name *string `json:"name" validate:"required" example:"test"` 14 | Code *string `json:"code" validate:"required" example:"test"` 15 | Metadata map[string]interface{} `json:"metadata" validate:"optional"` 16 | } 17 | 18 | func (req *EntitlementRegistrationRequest) Validate() error { 19 | if req.Name == nil { 20 | return cerrors.ErrEntitlementNameIsEmpty 21 | } 22 | 23 | if req.Code == nil { 24 | return cerrors.ErrEntitlementCodeIsEmpty 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func (req *EntitlementRegistrationRequest) ToEntitlementRegistrationInput(ctx context.Context, tracer trace.Tracer, entitlementURI entitlement_attribute.EntitlementCommonURI) *models.EntitlementRegistrationInput { 31 | return &models.EntitlementRegistrationInput{ 32 | TracerCtx: ctx, 33 | Tracer: tracer, 34 | Name: req.Name, 35 | Code: req.Code, 36 | Metadata: req.Metadata, 37 | EntitlementCommonURI: entitlementURI, 38 | } 39 | } 40 | 41 | type EntitlementRetrievalRequest struct { 42 | entitlement_attribute.EntitlementCommonURI 43 | } 44 | 45 | func (req *EntitlementRetrievalRequest) Validate() error { 46 | if req.EntitlementID == nil { 47 | return cerrors.ErrEntitlementIDIsEmpty 48 | } 49 | return req.EntitlementCommonURI.Validate() 50 | } 51 | 52 | func (req *EntitlementRetrievalRequest) ToEntitlementRetrievalInput(ctx context.Context, tracer trace.Tracer) *models.EntitlementRetrievalInput { 53 | return &models.EntitlementRetrievalInput{ 54 | TracerCtx: ctx, 55 | Tracer: tracer, 56 | EntitlementCommonURI: req.EntitlementCommonURI, 57 | } 58 | } 59 | 60 | type EntitlementDeletionRequest struct { 61 | entitlement_attribute.EntitlementCommonURI 62 | } 63 | 64 | func (req *EntitlementDeletionRequest) Validate() error { 65 | if req.EntitlementID == nil { 66 | return cerrors.ErrEntitlementIDIsEmpty 67 | } 68 | return req.EntitlementCommonURI.Validate() 69 | } 70 | 71 | func (req *EntitlementDeletionRequest) ToEntitlementDeletionInput(ctx context.Context, tracer trace.Tracer) *models.EntitlementDeletionInput { 72 | return &models.EntitlementDeletionInput{ 73 | TracerCtx: ctx, 74 | Tracer: tracer, 75 | EntitlementCommonURI: req.EntitlementCommonURI, 76 | } 77 | } 78 | 79 | type EntitlementListRequest struct { 80 | constants.QueryCommonParam 81 | } 82 | 83 | func (req *EntitlementListRequest) Validate() error { 84 | req.QueryCommonParam.Validate() 85 | return nil 86 | } 87 | 88 | func (req *EntitlementListRequest) ToEntitlementListInput(ctx context.Context, tracer trace.Tracer, uriParam entitlement_attribute.EntitlementCommonURI) *models.EntitlementListInput { 89 | return &models.EntitlementListInput{ 90 | TracerCtx: ctx, 91 | Tracer: tracer, 92 | EntitlementCommonURI: uriParam, 93 | QueryCommonParam: req.QueryCommonParam, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/middlewares/validate_permission_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "fmt" 5 | "github.com/casbin/casbin/v2" 6 | "github.com/gin-gonic/gin" 7 | "go-license-management/internal/cerrors" 8 | "go-license-management/internal/constants" 9 | "go-license-management/internal/infrastructure/casbin_adapter" 10 | "go-license-management/internal/infrastructure/logging" 11 | "go-license-management/internal/response" 12 | "net/http" 13 | "strings" 14 | ) 15 | 16 | func PermissionValidationMW(permission string) gin.HandlerFunc { 17 | return func(ctx *gin.Context) { 18 | 19 | e, err := casbin.NewEnforcer(casbin_adapter.GetEnforcerModel(), casbin_adapter.GetAdapter()) 20 | if err != nil { 21 | logging.GetInstance().GetLogger().Error(err.Error()) 22 | ctx.AbortWithStatusJSON( 23 | http.StatusInternalServerError, 24 | response.NewResponse(ctx).ToResponse( 25 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 26 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 27 | nil, 28 | nil, 29 | nil, 30 | ), 31 | ) 32 | return 33 | } 34 | 35 | err = e.LoadPolicy() 36 | if err != nil { 37 | logging.GetInstance().GetLogger().Error(err.Error()) 38 | ctx.AbortWithStatusJSON( 39 | http.StatusInternalServerError, 40 | response.NewResponse(ctx).ToResponse( 41 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 42 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 43 | nil, 44 | nil, 45 | nil, 46 | ), 47 | ) 48 | return 49 | } 50 | 51 | permObjects := strings.Split(permission, ".") 52 | 53 | ok, err := e.Enforce( 54 | ctx.GetString(constants.ContextValueTenant), 55 | ctx.GetString(constants.ContextValueSubject), 56 | permObjects[0], 57 | permObjects[1], 58 | ) 59 | if err != nil { 60 | logging.GetInstance().GetLogger().Error(err.Error()) 61 | ctx.AbortWithStatusJSON( 62 | http.StatusInternalServerError, 63 | response.NewResponse(ctx).ToResponse( 64 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 65 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 66 | nil, 67 | nil, 68 | nil, 69 | ), 70 | ) 71 | return 72 | } 73 | 74 | if !ok { 75 | logging.GetInstance().GetLogger().Info( 76 | fmt.Sprintf("invalid permission: domain [%s] | subject [%s] | object [%s] | action [%s]", 77 | ctx.GetString(constants.ContextValueTenant), 78 | ctx.GetString(constants.ContextValueSubject), 79 | permObjects[0], 80 | permObjects[1]), 81 | ) 82 | ctx.AbortWithStatusJSON( 83 | http.StatusForbidden, 84 | response.NewResponse(ctx).ToResponse( 85 | cerrors.ErrCodeMapper[cerrors.ErrGenericPermission], 86 | fmt.Sprintf("user [%s] does not have permission to perform the requested action", ctx.GetString(constants.ContextValueSubject)), 87 | nil, 88 | nil, 89 | nil, 90 | ), 91 | ) 92 | return 93 | } 94 | logging.GetInstance().GetLogger().Info( 95 | fmt.Sprintf("valid permission: domain [%s] | subject [%s] | object [%s] | action [%s]", 96 | ctx.GetString(constants.ContextValueTenant), 97 | ctx.GetString(constants.ContextValueSubject), 98 | permObjects[0], 99 | permObjects[1]), 100 | ) 101 | 102 | ctx.Next() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/services/v1/accounts/service/utils.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "go-license-management/internal/cerrors" 7 | "go-license-management/internal/constants" 8 | "go-license-management/internal/infrastructure/database/entities" 9 | "go-license-management/internal/utils" 10 | "time" 11 | ) 12 | 13 | func (svc *AccountService) actionBan(ctx *gin.Context, account *entities.Account) (*entities.Account, error) { 14 | svc.logger.GetLogger().Info(fmt.Sprintf("banning account [%s] in tenant [%s]", account.Username, account.Tenant)) 15 | account.BannedAt = time.Now() 16 | account.Status = constants.AccountStatusBanned 17 | 18 | account, err := svc.repo.UpdateAccountByPK(ctx, account) 19 | if err != nil { 20 | return account, err 21 | } 22 | 23 | return account, nil 24 | } 25 | 26 | func (svc *AccountService) actionUnban(ctx *gin.Context, account *entities.Account) (*entities.Account, error) { 27 | svc.logger.GetLogger().Info(fmt.Sprintf("unbanning account [%s] in tenant [%s]", account.Username, account.Tenant)) 28 | account.BannedAt = time.Now() 29 | account.Status = constants.AccountStatusActive 30 | 31 | account, err := svc.repo.UpdateAccountByPK(ctx, account) 32 | if err != nil { 33 | return account, err 34 | } 35 | 36 | return account, nil 37 | } 38 | 39 | func (svc *AccountService) actionGenerateResetToken(ctx *gin.Context, account *entities.Account) (*entities.Account, error) { 40 | svc.logger.GetLogger().Info(fmt.Sprintf("generate reset token for account [%s] in tenant [%s]", account.Username, account.TenantName)) 41 | 42 | account.PasswordResetSentAt = time.Now() 43 | account.PasswordResetToken = utils.RandStringBytesMaskImprSrcSB(32) 44 | account, err := svc.repo.UpdateAccountByPK(ctx, account) 45 | if err != nil { 46 | return account, err 47 | } 48 | return account, nil 49 | } 50 | 51 | func (svc *AccountService) actionResetPassword(ctx *gin.Context, token, newPass string, account *entities.Account) (*entities.Account, error) { 52 | svc.logger.GetLogger().Info(fmt.Sprintf("reset account [%s] in tenant [%s]", account.Username, account.TenantName)) 53 | 54 | if token != account.PasswordResetToken { 55 | return account, cerrors.ErrAccountResetTokenIsInvalid 56 | } 57 | 58 | if time.Now().After(account.PasswordResetSentAt.Add(24 * time.Hour)) { 59 | return account, cerrors.ErrAccountResetTokenIsExpired 60 | } 61 | 62 | newHash, err := utils.HashPassword(newPass) 63 | if err != nil { 64 | return account, err 65 | } 66 | 67 | account.PasswordDigest = newHash 68 | account, err = svc.repo.UpdateAccountByPK(ctx, account) 69 | if err != nil { 70 | return account, err 71 | } 72 | return account, nil 73 | } 74 | 75 | func (svc *AccountService) actionUpdatePassword(ctx *gin.Context, currentPass, newPass string, account *entities.Account) (*entities.Account, error) { 76 | svc.logger.GetLogger().Info(fmt.Sprintf("updating account [%s] in tenant [%s]", account.Username, account.TenantName)) 77 | 78 | if !utils.CompareHashedPassword(account.PasswordDigest, currentPass) { 79 | return account, cerrors.ErrAccountPasswordNotMatch 80 | } 81 | 82 | newHash, err := utils.HashPassword(newPass) 83 | if err != nil { 84 | return account, err 85 | } 86 | 87 | account.PasswordDigest = newHash 88 | account, err = svc.repo.UpdateAccountByPK(ctx, account) 89 | if err != nil { 90 | return account, err 91 | } 92 | 93 | return account, nil 94 | 95 | } 96 | -------------------------------------------------------------------------------- /internal/services/v1/policies/service/utils.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/database/entities" 8 | "go-license-management/internal/services/v1/policies/models" 9 | "go-license-management/internal/utils" 10 | ) 11 | 12 | func (svc *PolicyService) updatePolicyField(ctx *gin.Context, input *models.PolicyUpdateInput, policy *entities.Policy) (*entities.Policy, error) { 13 | var err error 14 | 15 | // Generate new private/public key pair 16 | if input.Scheme != nil { 17 | scheme := utils.DerefPointer(input.Scheme) 18 | if policy.Scheme != scheme { 19 | var privateKey = "" 20 | var publicKey = "" 21 | svc.logger.GetLogger().Info(fmt.Sprintf("generating private/public key pair using [%s] algorithm", scheme)) 22 | switch scheme { 23 | case constants.PolicySchemeED25519: 24 | privateKey, publicKey, err = utils.NewEd25519KeyPair() 25 | if err != nil { 26 | svc.logger.GetLogger().Error(err.Error()) 27 | return policy, err 28 | } 29 | case constants.PolicySchemeRSA2048PKCS1: 30 | privateKey, publicKey, err = utils.NewRSA2048PKCS1KeyPair() 31 | if err != nil { 32 | svc.logger.GetLogger().Error(err.Error()) 33 | return policy, err 34 | } 35 | default: 36 | svc.logger.GetLogger().Error(fmt.Sprintf("invalid supported sheme [%s]", scheme)) 37 | return policy, err 38 | } 39 | policy.PrivateKey = privateKey 40 | policy.PublicKey = publicKey 41 | } 42 | } 43 | 44 | if input.Duration != nil { 45 | policy.Duration = utils.DerefPointer(input.Duration) 46 | } 47 | 48 | if input.MaxMachines != nil { 49 | policy.MaxMachines = utils.DerefPointer(input.MaxMachines) 50 | } 51 | 52 | if input.MaxUses != nil { 53 | policy.MaxUses = utils.DerefPointer(input.MaxUses) 54 | } 55 | 56 | if input.HeartbeatDuration != nil { 57 | policy.HeartbeatDuration = utils.DerefPointer(input.HeartbeatDuration) 58 | } 59 | 60 | if input.MaxUsers != nil { 61 | policy.MaxUsers = utils.DerefPointer(input.MaxUsers) 62 | } 63 | 64 | if input.ExpirationStrategy != nil { 65 | policy.ExpirationStrategy = utils.DerefPointer(input.ExpirationStrategy) 66 | } 67 | 68 | if input.OverageStrategy != nil { 69 | policy.OverageStrategy = utils.DerefPointer(input.OverageStrategy) 70 | } 71 | 72 | if input.RenewalBasis != nil { 73 | policy.RenewalBasis = utils.DerefPointer(input.RenewalBasis) 74 | } 75 | 76 | if input.HeartbeatBasis != nil { 77 | policy.HeartbeatBasis = utils.DerefPointer(input.HeartbeatBasis) 78 | } 79 | 80 | if input.CheckInInterval != nil { 81 | policy.CheckInInterval = utils.DerefPointer(input.CheckInInterval) 82 | } 83 | 84 | if input.RequireCheckIn != nil { 85 | policy.RequireCheckIn = utils.DerefPointer(input.RequireCheckIn) 86 | } 87 | if input.RequireHeartbeat != nil { 88 | policy.RequireHeartbeat = utils.DerefPointer(input.RequireHeartbeat) 89 | } 90 | if input.UsePool != nil { 91 | policy.UsePool = utils.DerefPointer(input.UsePool) 92 | } 93 | if input.Protected != nil { 94 | policy.Protected = utils.DerefPointer(input.Protected) 95 | } 96 | if input.RateLimited != nil { 97 | policy.RateLimited = utils.DerefPointer(input.RateLimited) 98 | } 99 | if input.Encrypted != nil { 100 | policy.Encrypted = utils.DerefPointer(input.Encrypted) 101 | } 102 | 103 | return policy, nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/utils/ed25519.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/x509" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "strings" 11 | ) 12 | 13 | // NewEd25519KeyPair generates the private signing key and the public verify key using Ed25519 algorithm 14 | // Return te signingKey (private key) and verifyKey (public key) 15 | func NewEd25519KeyPair() (string, string, error) { 16 | publicKey, privateKey, err := ed25519.GenerateKey(nil) 17 | if err != nil { 18 | return "", "", err 19 | } 20 | 21 | // Export the private key in PKCS#8 DER format 22 | privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) 23 | if err != nil { 24 | return "", "", err 25 | } 26 | signingKey := base64.StdEncoding.EncodeToString(privateKeyBytes) 27 | 28 | // Export the public key in SPKI DER format 29 | publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) 30 | if err != nil { 31 | return "", "", err 32 | } 33 | verifyKey := base64.StdEncoding.EncodeToString(publicKeyBytes) 34 | 35 | return signingKey, verifyKey, nil 36 | } 37 | 38 | // NewLicenseKeyWithEd25519 generates new license key using Ed25519 algorithm 39 | // Returns a license string in format {{signature}}.{{data}} 40 | func NewLicenseKeyWithEd25519(signingKey string, data any) (string, error) { 41 | privateKeyBytes, err := base64.StdEncoding.DecodeString(signingKey) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | decodedPrivateKey, err := x509.ParsePKCS8PrivateKey(privateKeyBytes) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | // Assert that it is of type ed25519.PrivateKey 52 | privateKey, ok := decodedPrivateKey.(ed25519.PrivateKey) 53 | if !ok { 54 | return "", errors.New("decoded key is not of type ed25519.PrivateKey") 55 | } 56 | 57 | bData, err := json.Marshal(data) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | // Sign the data with the private key 63 | signature := ed25519.Sign(privateKey, bData) 64 | encodedSignature := base64.StdEncoding.EncodeToString(signature) 65 | encodedData := base64.StdEncoding.EncodeToString(bData) 66 | 67 | // Combine the encoded data and signature to create the license key 68 | licenseKey := fmt.Sprintf("%s.%s", encodedSignature, encodedData) 69 | 70 | return licenseKey, nil 71 | } 72 | 73 | // VerifyLicenseKeyWithEd25519 verifies a license key against the provided public key using Ed25519 algorithm 74 | func VerifyLicenseKeyWithEd25519(verifyKey string, licenseKey string) (bool, []byte, error) { 75 | parts := strings.Split(licenseKey, ".") 76 | if len(parts) != 2 { 77 | return false, nil, errors.New("invalid license key format") 78 | } 79 | encodedSignature := parts[0] 80 | encodedData := parts[1] 81 | 82 | data, err := base64.StdEncoding.DecodeString(encodedData) 83 | if err != nil { 84 | return false, nil, err 85 | } 86 | 87 | signature, err := base64.StdEncoding.DecodeString(encodedSignature) 88 | if err != nil { 89 | return false, nil, err 90 | } 91 | 92 | publicKeyBytes, err := base64.StdEncoding.DecodeString(verifyKey) 93 | if err != nil { 94 | return false, nil, err 95 | } 96 | 97 | decodedPublicKey, err := x509.ParsePKIXPublicKey(publicKeyBytes) 98 | if err != nil { 99 | return false, nil, err 100 | } 101 | 102 | publicKey, ok := decodedPublicKey.(ed25519.PublicKey) 103 | if !ok { 104 | return false, nil, errors.New("decoded key is not of type ed25519.PublicKey") 105 | } 106 | 107 | return ed25519.Verify(publicKey, data, signature), data, nil 108 | } 109 | -------------------------------------------------------------------------------- /internal/infrastructure/models/machine_attribute/struct.go: -------------------------------------------------------------------------------- 1 | package machine_attribute 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/utils" 8 | "time" 9 | ) 10 | 11 | type MachineCommonURI struct { 12 | TenantName *string `uri:"tenant_name"` 13 | MachineID *string `uri:"machine_id"` 14 | MachineAction *string `uri:"machine_action"` 15 | } 16 | 17 | func (req *MachineCommonURI) Validate() error { 18 | if req.TenantName == nil { 19 | return cerrors.ErrTenantNameIsEmpty 20 | } 21 | 22 | if req.MachineID != nil { 23 | if _, err := uuid.Parse(utils.DerefPointer(req.MachineID)); err != nil { 24 | return cerrors.ErrMachineIDIsInvalid 25 | } 26 | } 27 | 28 | if req.MachineAction != nil { 29 | if _, ok := constants.ValidMachineActionsMapper[utils.DerefPointer(req.MachineAction)]; !ok { 30 | return cerrors.ErrMachineActionIsInvalid 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | 37 | type MachineActionsQueryParam struct { 38 | TTL *int `form:"ttl"` 39 | ToFile *bool `form:"to_file"` 40 | } 41 | 42 | func (req *MachineActionsQueryParam) Validate() error { 43 | if req.TTL == nil { 44 | req.TTL = utils.RefPointer(constants.DefaultLicenseTTL) 45 | } else { 46 | ttl := utils.DerefPointer(req.TTL) 47 | if ttl < constants.MinimumLicenseTTL || ttl > constants.MaximumLicenseTTL { 48 | return cerrors.ErrMachineActionCheckoutTTLIsInvalid 49 | } 50 | } 51 | 52 | if req.ToFile == nil { 53 | req.ToFile = utils.RefPointer(false) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // MachineAttributeModel contains information about the machine. Machines can be used to track and manage where your users are allowed to use your product. 60 | type MachineAttributeModel struct { 61 | LicenseKey *string `json:"license_key"` // The license key associated with the machine 62 | Fingerprint *string `json:"fingerprint"` // The fingerprint of the machine. This can be an arbitrary string, but must be unique within the scope of the license it belongs to. 63 | Cores *int `json:"cores"` // The number of CPU cores for the machine. 64 | Name *string `json:"name"` // The human-readable name of the machine. 65 | IP *string `json:"ip"` // The IP of the machine. 66 | Hostname *string `json:"hostname"` // The hostname of the machine. 67 | Platform *string `json:"platform"` // The platform of the machine. 68 | Metadata map[string]interface{} `json:"metadata"` // Object containing machine metadata. 69 | } 70 | 71 | // MachineLicenseField contains information about the license 72 | type MachineLicenseField struct { 73 | TenantName string `json:"tenant_name"` 74 | ProductID string `json:"product_id"` 75 | PolicyID string `json:"policy_id"` 76 | LicenseID string `json:"license_id"` 77 | MachineFingerprint string `json:"machine_fingerprint"` 78 | Metadata map[string]interface{} `json:"metadata"` 79 | TTL int `json:"ttl"` 80 | Expiry time.Time `json:"expiry"` 81 | CreatedAt time.Time `json:"created_at"` 82 | } 83 | 84 | // MachineLicenseFileContent contains information about the license file 85 | type MachineLicenseFileContent struct { 86 | Enc string `json:"enc"` 87 | Sig string `json:"sig"` 88 | Alg string `json:"alg"` 89 | } 90 | -------------------------------------------------------------------------------- /internal/services/v1/machines/models/machine_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/models/machine_attribute" 8 | "go.opentelemetry.io/otel/trace" 9 | "time" 10 | ) 11 | 12 | type MachineRegistrationInput struct { 13 | TracerCtx context.Context 14 | Tracer trace.Tracer 15 | machine_attribute.MachineCommonURI 16 | machine_attribute.MachineAttributeModel 17 | } 18 | 19 | type MachineInfoOutput struct { 20 | ID uuid.UUID `json:"id"` 21 | LicenseKey string `json:"license_key"` 22 | TenantName string `json:"tenant_name"` 23 | Fingerprint string `json:"fingerprint"` 24 | IP string `json:"ip"` 25 | Hostname string `json:"hostname"` 26 | Platform string `json:"platform"` 27 | Name string `json:"name"` 28 | Metadata map[string]interface{} `json:"metadata"` 29 | Cores int `json:"cores"` 30 | LastHeartbeatAt time.Time `json:"last_heartbeat_at"` 31 | LastCheckOutAt time.Time `json:"last_check_out_at"` 32 | CreatedAt time.Time `json:"created_at"` 33 | UpdatedAt time.Time `json:"updated_at"` 34 | } 35 | 36 | type MachineUpdateInput struct { 37 | TracerCtx context.Context 38 | Tracer trace.Tracer 39 | machine_attribute.MachineCommonURI 40 | machine_attribute.MachineAttributeModel 41 | } 42 | 43 | type MachineRetrievalInput struct { 44 | TracerCtx context.Context 45 | Tracer trace.Tracer 46 | machine_attribute.MachineCommonURI 47 | } 48 | 49 | type MachineDeleteInput struct { 50 | TracerCtx context.Context 51 | Tracer trace.Tracer 52 | machine_attribute.MachineCommonURI 53 | } 54 | 55 | type MachineListInput struct { 56 | TracerCtx context.Context 57 | Tracer trace.Tracer 58 | machine_attribute.MachineCommonURI 59 | constants.QueryCommonParam 60 | } 61 | 62 | type MachineListOutput struct { 63 | ID uuid.UUID `json:"id"` 64 | LicenseID uuid.UUID `json:"license_id"` 65 | LicenseKey string `json:"license_key"` 66 | TenantName string `json:"tenant_name"` 67 | Fingerprint string `json:"fingerprint"` 68 | IP string `json:"ip"` 69 | Hostname string `json:"hostname"` 70 | Platform string `json:"platform"` 71 | Name string `json:"name"` 72 | Metadata map[string]interface{} `json:"metadata"` 73 | Cores int `json:"cores"` 74 | LastHeartbeatAt time.Time `json:"last_heartbeat_at"` 75 | LastDeathEventSentAt time.Time `json:"last_death_event_sent_at"` 76 | LastCheckOutAt time.Time `json:"last_check_out_at"` 77 | CreatedAt time.Time `json:"created_at"` 78 | UpdatedAt time.Time `json:"updated_at"` 79 | } 80 | 81 | type MachineActionsInput struct { 82 | TracerCtx context.Context 83 | Tracer trace.Tracer 84 | machine_attribute.MachineCommonURI 85 | machine_attribute.MachineActionsQueryParam 86 | } 87 | 88 | type MachineActionCheckoutOutput struct { 89 | Certificate string `json:"certificate"` 90 | TTL int `json:"ttl"` 91 | IssuedAt time.Time `json:"issued_at"` 92 | ExpiresAt time.Time `json:"expires_at"` 93 | } 94 | -------------------------------------------------------------------------------- /internal/repositories/v1/entitlements/repository.go: -------------------------------------------------------------------------------- 1 | package entitlements 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "github.com/uptrace/bun" 7 | "go-license-management/internal/cerrors" 8 | "go-license-management/internal/constants" 9 | "go-license-management/internal/infrastructure/database/entities" 10 | "go-license-management/internal/utils" 11 | "go-license-management/server/api" 12 | ) 13 | 14 | type EntitlementRepository struct { 15 | database *bun.DB 16 | } 17 | 18 | func NewEntitlementRepository(ds *api.DataSource) *EntitlementRepository { 19 | return &EntitlementRepository{ 20 | database: ds.GetDatabase(), 21 | } 22 | } 23 | 24 | func (repo *EntitlementRepository) SelectTenantByPK(ctx context.Context, tenantName string) (*entities.Tenant, error) { 25 | if repo.database == nil { 26 | return nil, cerrors.ErrInvalidDatabaseClient 27 | } 28 | 29 | tenant := &entities.Tenant{Name: tenantName} 30 | 31 | err := repo.database.NewSelect().Model(tenant).WherePK().Scan(ctx) 32 | if err != nil { 33 | return tenant, err 34 | } 35 | 36 | return tenant, nil 37 | } 38 | 39 | func (repo *EntitlementRepository) CheckEntitlementExistByCode(ctx context.Context, code string) (bool, error) { 40 | if repo.database == nil { 41 | return false, cerrors.ErrInvalidDatabaseClient 42 | } 43 | 44 | entitlement := &entities.Entitlement{ 45 | Code: code, 46 | } 47 | exist, err := repo.database.NewSelect().Model(entitlement).Where("code = ?", code).Exists(ctx) 48 | if err != nil { 49 | return exist, err 50 | } 51 | 52 | return exist, nil 53 | } 54 | 55 | func (repo *EntitlementRepository) InsertNewEntitlement(ctx context.Context, entitlement *entities.Entitlement) error { 56 | if repo.database == nil { 57 | return cerrors.ErrInvalidDatabaseClient 58 | } 59 | 60 | _, err := repo.database.NewInsert().Model(entitlement).Exec(ctx) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (repo *EntitlementRepository) SelectEntitlementByPK(ctx context.Context, entitlementID uuid.UUID) (*entities.Entitlement, error) { 69 | if repo.database == nil { 70 | return nil, cerrors.ErrInvalidDatabaseClient 71 | } 72 | 73 | entitlement := &entities.Entitlement{ID: entitlementID} 74 | err := repo.database.NewSelect().Model(entitlement).WherePK().Scan(ctx) 75 | if err != nil { 76 | return entitlement, err 77 | } 78 | return entitlement, nil 79 | } 80 | 81 | func (repo *EntitlementRepository) DeleteEntitlementByPK(ctx context.Context, entitlementID uuid.UUID) error { 82 | if repo.database == nil { 83 | return cerrors.ErrInvalidDatabaseClient 84 | } 85 | 86 | entitlement := &entities.Entitlement{ID: entitlementID} 87 | _, err := repo.database.NewDelete().Model(entitlement).WherePK().Exec(ctx) 88 | if err != nil { 89 | return err 90 | } 91 | return nil 92 | } 93 | 94 | func (repo *EntitlementRepository) SelectEntitlementsByTenant(ctx context.Context, tenantName string, param constants.QueryCommonParam) ([]entities.Entitlement, int, error) { 95 | var total = 0 96 | if repo.database == nil { 97 | return nil, total, cerrors.ErrInvalidDatabaseClient 98 | } 99 | 100 | entitlements := make([]entities.Entitlement, 0) 101 | total, err := repo.database.NewSelect().Model(new(entities.Entitlement)). 102 | Where("tenant_name = ?", tenantName). 103 | Limit(utils.DerefPointer(param.Limit)). 104 | Offset(utils.DerefPointer(param.Offset)). 105 | Order("created_at DESC"). 106 | ScanAndCount(ctx, &entitlements) 107 | if err != nil { 108 | return entitlements, total, cerrors.ErrInvalidDatabaseClient 109 | } 110 | return entitlements, total, nil 111 | } 112 | -------------------------------------------------------------------------------- /internal/infrastructure/database/entities/policy.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/uptrace/bun" 6 | "time" 7 | ) 8 | 9 | type Policy struct { 10 | bun.BaseModel `bun:"table:policies,alias:p" swaggerignore:"true"` 11 | 12 | ID uuid.UUID `bun:"id,pk,type:uuid"` 13 | ProductID uuid.UUID `bun:"product_id,type:uuid"` 14 | TenantName string `bun:"tenant_name,type:varchar(256),notnull"` 15 | PublicKey string `bun:"public_key,type:varchar(4096),notnull"` 16 | PrivateKey string `bun:"private_key,type:varchar(4096),notnull"` 17 | Name string `bun:"name,type:varchar(256),nullzero"` 18 | Scheme string `bun:"scheme,type:varchar(128),nullzero"` 19 | ExpirationStrategy string `bun:"expiration_strategy,type:varchar(64),nullzero"` 20 | CheckInInterval string `bun:"check_in_interval,type:varchar(64),nullzero"` 21 | OverageStrategy string `bun:"overage_strategy,type:varchar(64),nullzero"` 22 | HeartbeatBasis string `bun:"heartbeat_basis,type:varchar(64),nullzero"` 23 | RenewalBasis string `bun:"renewal_basis,type:varchar(64),nullzero"` 24 | Duration int64 `bun:"duration,nullzero"` 25 | MaxMachines int `bun:"max_machines,nullzero"` 26 | MaxUses int `bun:"max_uses,nullzero"` 27 | MaxUsers int `bun:"max_users,nullzero"` 28 | HeartbeatDuration int `bun:"heartbeat_duration,nullzero"` 29 | Strict bool `bun:"strict,default:false"` 30 | Floating bool `bun:"floating,default:false"` 31 | UsePool bool `bun:"use_pool,default:false"` 32 | RateLimited bool `bun:"rate_limited,default:false"` 33 | Encrypted bool `bun:"encrypted,default:false"` 34 | Protected bool `bun:"protected,default:false"` 35 | RequireCheckIn bool `bun:"require_check_in,default:false"` 36 | RequireHeartbeat bool `bun:"require_heartbeat,default:false,notnull"` 37 | Metadata map[string]interface{} `bun:"type:jsonb,nullzero"` 38 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 39 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 40 | Tenant *Tenant `bun:"rel:belongs-to,join:tenant_name=name"` 41 | Product *Product `bun:"rel:belongs-to,join:product_id=id"` 42 | } 43 | 44 | type PolicyEntitlement struct { 45 | bun.BaseModel `bun:"table:policy_entitlements,alias:pe" swaggerignore:"true"` 46 | 47 | ID uuid.UUID `bun:"id,pk,type:uuid"` 48 | TenantName string `bun:"tenant_name,type:varchar(256),notnull"` 49 | PolicyID uuid.UUID `bun:"policy_id,type:uuid,notnull"` 50 | EntitlementID uuid.UUID `bun:"entitlement_id,type:uuid,notnull"` 51 | Metadata map[string]interface{} `bun:"type:jsonb,nullzero"` 52 | CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` 53 | UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` 54 | Tenant *Tenant `bun:"rel:belongs-to,join:tenant_name=name"` 55 | Policy *Policy `bun:"rel:belongs-to,join:policy_id=id"` 56 | Entitlement *Entitlement `bun:"rel:belongs-to,join:entitlement_id=id"` 57 | } 58 | -------------------------------------------------------------------------------- /internal/services/v1/products/models/product_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/models/product_attribute" 8 | "go.opentelemetry.io/otel/trace" 9 | "time" 10 | ) 11 | 12 | type ProductRegistrationInput struct { 13 | TracerCtx context.Context 14 | Tracer trace.Tracer 15 | product_attribute.ProductCommonURI 16 | product_attribute.ProductAttribute 17 | } 18 | 19 | type ProductRegistrationOutput struct { 20 | ID string `json:"id"` 21 | TenantName string `json:"tenant_name"` 22 | Name string `json:"name"` 23 | DistributionStrategy string `json:"distribution_strategy"` 24 | Code string `json:"code"` 25 | URL string `json:"url"` 26 | Platforms []string `json:"platform"` 27 | Metadata map[string]interface{} `json:"metadata"` 28 | CreatedAt time.Time `json:"created_at"` 29 | UpdatedAt time.Time `json:"updated_at"` 30 | } 31 | 32 | type ProductUpdateInput struct { 33 | TracerCtx context.Context 34 | Tracer trace.Tracer 35 | product_attribute.ProductCommonURI 36 | product_attribute.ProductAttribute 37 | } 38 | 39 | type ProductUpdateOutput struct { 40 | ID string `json:"id"` 41 | TenantName string `json:"tenant_name"` 42 | Name string `json:"name"` 43 | DistributionStrategy string `json:"distribution_strategy"` 44 | Code string `json:"code"` 45 | URL string `json:"url"` 46 | Platforms []string `json:"platform"` 47 | Metadata map[string]interface{} `json:"metadata"` 48 | CreatedAt time.Time `json:"created_at"` 49 | UpdatedAt time.Time `json:"updated_at"` 50 | } 51 | 52 | type ProductListInput struct { 53 | TracerCtx context.Context 54 | Tracer trace.Tracer 55 | product_attribute.ProductCommonURI 56 | constants.QueryCommonParam 57 | } 58 | 59 | type ProductRetrievalInput struct { 60 | TracerCtx context.Context 61 | Tracer trace.Tracer 62 | product_attribute.ProductCommonURI 63 | } 64 | 65 | type ProductRetrievalOutput struct { 66 | ID uuid.UUID `json:"id"` 67 | TenantName string `json:"tenant_name"` 68 | Name string `json:"name"` 69 | DistributionStrategy string `json:"distribution_strategy"` 70 | Code string `json:"code"` 71 | Platforms []string `json:"platform"` 72 | Metadata map[string]interface{} `json:"metadata"` 73 | URL string `json:"url,type"` 74 | CreatedAt time.Time `json:"created_at"` 75 | UpdatedAt time.Time `json:"updated_at"` 76 | } 77 | 78 | type ProductDeletionInput struct { 79 | TracerCtx context.Context 80 | Tracer trace.Tracer 81 | product_attribute.ProductCommonURI 82 | } 83 | 84 | type ProductTokensInput struct { 85 | TracerCtx context.Context 86 | Tracer trace.Tracer 87 | product_attribute.ProductCommonURI 88 | Name *string `json:"name" validate:"optional" example:"test"` 89 | Expiry *string `json:"expiry" validate:"optional" example:"test"` 90 | Permissions []string `json:"permissions" validate:"required" example:"test"` 91 | } 92 | 93 | type ProductTokenOutput struct { 94 | ID string `json:"id"` 95 | Token string `json:"token"` 96 | } 97 | -------------------------------------------------------------------------------- /internal/services/v1/authentications/service/utils.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/x509" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "errors" 9 | "github.com/gin-gonic/gin" 10 | "github.com/golang-jwt/jwt/v5" 11 | "go-license-management/internal/constants" 12 | "go-license-management/internal/infrastructure/database/entities" 13 | "go-license-management/internal/permissions" 14 | "time" 15 | ) 16 | 17 | func (svc *AuthenticationService) generateSuperadminJWT(ctx *gin.Context, master *entities.Master) (string, int64, error) { 18 | 19 | jwtPermissions := make([]string, 0) 20 | for k, _ := range permissions.SuperAdminPermissionMapper { 21 | jwtPermissions = append(jwtPermissions, k) 22 | } 23 | 24 | now := time.Now() 25 | exp := now.Add(time.Hour).Unix() 26 | claims := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{ 27 | "sub": master.Username, // Subject (user identifier) 28 | "iss": constants.AppName, // Issuer 29 | "aud": master.RoleName, // Audience (user role) 30 | "exp": exp, // Expiration time 31 | "iat": now.Unix(), 32 | "nbf": now.Unix(), 33 | "tenant": "*", 34 | "status": constants.AccountStatusActive, 35 | "permissions": jwtPermissions, 36 | }) 37 | 38 | privateKey, err := base64.StdEncoding.DecodeString(master.Ed25519PrivateKey) 39 | if err != nil { 40 | return "", 0, err 41 | } 42 | 43 | decodedPrivateKey, err := x509.ParsePKCS8PrivateKey(privateKey) 44 | if err != nil { 45 | return "", 0, err 46 | } 47 | 48 | privateKey, ok := decodedPrivateKey.(ed25519.PrivateKey) 49 | if !ok { 50 | return "", 0, errors.New("decoded key is not of type ed25519.PrivateKey") 51 | } 52 | 53 | tokenString, err := claims.SignedString(decodedPrivateKey) 54 | if err != nil { 55 | return "", 0, err 56 | } 57 | 58 | return tokenString, exp, nil 59 | } 60 | 61 | func (svc *AuthenticationService) generateJWT(ctx *gin.Context, tenant *entities.Tenant, account *entities.Account) (string, int64, error) { 62 | 63 | jwtPermissions := make([]string, 0) 64 | switch account.RoleName { 65 | case constants.RoleUser: 66 | for k, _ := range permissions.UserPermissionMapper { 67 | jwtPermissions = append(jwtPermissions, k) 68 | } 69 | case constants.RoleAdmin: 70 | for k, _ := range permissions.AdminPermissionMapper { 71 | jwtPermissions = append(jwtPermissions, k) 72 | } 73 | case constants.RoleSuperAdmin: 74 | for k, _ := range permissions.SuperAdminPermissionMapper { 75 | jwtPermissions = append(jwtPermissions, k) 76 | } 77 | } 78 | 79 | now := time.Now() 80 | exp := now.Add(time.Hour).Unix() 81 | claims := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{ 82 | "sub": account.Username, // Subject (user identifier) 83 | "iss": constants.AppName, // Issuer 84 | "aud": account.RoleName, // Audience (user role) 85 | "exp": exp, // Expiration time 86 | "iat": now.Unix(), 87 | "nbf": now.Unix(), 88 | "tenant": account.TenantName, 89 | "status": account.Status, 90 | "permissions": jwtPermissions, 91 | }) 92 | 93 | privateKey, err := hex.DecodeString(tenant.Ed25519PrivateKey) 94 | if err != nil { 95 | return "", 0, err 96 | } 97 | 98 | decodedPrivateKey, err := x509.ParsePKCS8PrivateKey(privateKey) 99 | if err != nil { 100 | return "", 0, err 101 | } 102 | 103 | privateKey, ok := decodedPrivateKey.(ed25519.PrivateKey) 104 | if !ok { 105 | return "", 0, errors.New("decoded key is not of type ed25519.PrivateKey") 106 | } 107 | 108 | tokenString, err := claims.SignedString(decodedPrivateKey) 109 | if err != nil { 110 | return "", 0, err 111 | } 112 | 113 | return tokenString, exp, nil 114 | } 115 | -------------------------------------------------------------------------------- /internal/infrastructure/models/policy_attribute/struct.go: -------------------------------------------------------------------------------- 1 | package policy_attribute 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/utils" 7 | ) 8 | 9 | type PolicyCommonURI struct { 10 | TenantName *string `uri:"tenant_name"` 11 | PolicyID *string `uri:"policy_id"` 12 | } 13 | 14 | func (req *PolicyCommonURI) Validate() error { 15 | if req.TenantName == nil { 16 | return cerrors.ErrTenantNameIsEmpty 17 | } 18 | 19 | if req.PolicyID != nil { 20 | if _, err := uuid.Parse(utils.DerefPointer(req.PolicyID)); err != nil { 21 | return cerrors.ErrPolicyIDIsInvalid 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | 28 | type PolicyAttributeModel struct { 29 | Name *string `json:"name" validate:"required"` // Name: name of the policy 30 | Scheme *string `json:"scheme" validate:"optional"` // Scheme: The encryption/signature scheme used on license keys. 31 | Strict *bool `json:"strict" validate:"optional"` // Strict: All categories must valid in order for the license to be considered valid. Default: false 32 | RateLimited *bool `json:"rate_limited" validate:"optional"` // RateLimited: Whether the policy is for rate limiting feature. Default: false 33 | Floating *bool `json:"floating" validate:"optional"` // Floating: When true, license that implements the policy will be valid across multiple machines. Default: false 34 | UsePool *bool `json:"use_pool" validate:"optional"` // UsePool: Whether to pull license keys from a finite pool of pre-determined keys 35 | Encrypted *bool `json:"encrypted" validate:"optional"` // Encrypted: Whether to encrypt the license file 36 | Protected *bool `json:"protected" validate:"optional"` // Protected: Whether the policy is protected. 37 | RequireCheckIn *bool `json:"require_check_in" validate:"optional"` // RequireCheckIn: When true, require check-in at a predefined interval to continue to pass validation. Default: false 38 | RequireHeartbeat *bool `json:"require_heartbeat" validate:"optional"` // RequireHeartbeat: Whether the policy requires its machines to maintain a heartbeat. 39 | MaxMachines *int `json:"max_machines" validate:"optional"` // MaxMachines: The maximum number of machines a license implementing the policy can have associated with it 40 | MaxUsers *int `json:"max_users" validate:"optional"` // MaxUsers: The maximum number of users a license implementing the policy can have associated with it 41 | MaxUses *int `json:"max_uses" validate:"optional"` // MaxUses: The maximum number of uses a license implementing the policy can have. 42 | HeartbeatDuration *int `json:"heartbeat_duration" validate:"optional"` // HeartbeatDuration: The heartbeat duration for the policy, in seconds. 43 | Duration *int64 `json:"duration" validate:"optional"` // Duration: The length of time that a policy is valid 44 | CheckInInterval *string `json:"check_in_interval" validate:"optional"` // CheckInInterval: The time duration between each checkin 45 | HeartbeatBasis *string `json:"heartbeat_basis" validate:"optional"` // HeartbeatBasis: Control when a machine's initial heartbeat is started. 46 | ExpirationStrategy *string `json:"expiration_strategy" validate:"optional"` // ExpirationStrategy: The strategy for expired licenses during a license validation. 47 | RenewalBasis *string `json:"renewal_basis" validate:"optional"` // RenewalBasis: Control how a license's expiry is extended during renewal. 48 | OverageStrategy *string `json:"overage_strategy" validate:"optional"` // OverageStrategy: The strategy used for allowing machine overages. 49 | Metadata map[string]interface{} `json:"metadata" validate:"optional"` // Metadata: Policy metadata. 50 | } 51 | -------------------------------------------------------------------------------- /internal/repositories/v1/products/repository.go: -------------------------------------------------------------------------------- 1 | package products 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "github.com/uptrace/bun" 7 | "go-license-management/internal/cerrors" 8 | "go-license-management/internal/constants" 9 | "go-license-management/internal/infrastructure/database/entities" 10 | "go-license-management/internal/utils" 11 | "go-license-management/server/api" 12 | "time" 13 | ) 14 | 15 | type ProductRepository struct { 16 | database *bun.DB 17 | } 18 | 19 | func NewProductRepository(ds *api.DataSource) *ProductRepository { 20 | return &ProductRepository{ 21 | database: ds.GetDatabase(), 22 | } 23 | } 24 | 25 | func (repo *ProductRepository) SelectTenantByPK(ctx context.Context, tenantName string) (*entities.Tenant, error) { 26 | if repo.database == nil { 27 | return nil, cerrors.ErrInvalidDatabaseClient 28 | } 29 | 30 | tenant := &entities.Tenant{Name: tenantName} 31 | 32 | err := repo.database.NewSelect().Model(tenant).WherePK().Scan(ctx) 33 | if err != nil { 34 | return tenant, err 35 | } 36 | 37 | return tenant, nil 38 | } 39 | 40 | func (repo *ProductRepository) InsertNewProduct(ctx context.Context, product *entities.Product) error { 41 | if repo.database == nil { 42 | return cerrors.ErrInvalidDatabaseClient 43 | } 44 | 45 | _, err := repo.database.NewInsert().Model(product).Exec(ctx) 46 | if err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | func (repo *ProductRepository) SelectProductByPK(ctx context.Context, productID uuid.UUID) (*entities.Product, error) { 53 | if repo.database == nil { 54 | return nil, cerrors.ErrInvalidDatabaseClient 55 | } 56 | 57 | product := &entities.Product{ID: productID} 58 | err := repo.database.NewSelect().Model(product).WherePK().Scan(ctx) 59 | if err != nil { 60 | return product, err 61 | } 62 | return product, nil 63 | } 64 | 65 | func (repo *ProductRepository) SelectProducts(ctx context.Context, tenantName string, queryParam constants.QueryCommonParam) ([]entities.Product, int, error) { 66 | var total = 0 67 | 68 | if repo.database == nil { 69 | return nil, total, cerrors.ErrInvalidDatabaseClient 70 | } 71 | 72 | products := make([]entities.Product, 0) 73 | total, err := repo.database.NewSelect().Model(new(entities.Product)). 74 | Where("tenant_name = ?", tenantName). 75 | Order("created_at DESC"). 76 | Limit(utils.DerefPointer(queryParam.Limit)). 77 | Offset(utils.DerefPointer(queryParam.Offset)). 78 | ScanAndCount(ctx, &products) 79 | if err != nil { 80 | return products, total, err 81 | } 82 | return products, total, nil 83 | } 84 | 85 | func (repo *ProductRepository) CheckProductExistByCode(ctx context.Context, code string) (bool, error) { 86 | if repo.database == nil { 87 | return false, cerrors.ErrInvalidDatabaseClient 88 | } 89 | 90 | product := &entities.Product{Code: code} 91 | exist, err := repo.database.NewSelect().Model(product).Where("code = ?", code).Exists(ctx) 92 | if err != nil { 93 | return exist, err 94 | } 95 | return exist, nil 96 | } 97 | 98 | func (repo *ProductRepository) DeleteProductByPK(ctx context.Context, productID uuid.UUID) error { 99 | if repo.database == nil { 100 | return cerrors.ErrInvalidDatabaseClient 101 | } 102 | 103 | product := &entities.Product{ID: productID} 104 | _, err := repo.database.NewDelete().Model(product).WherePK().Exec(ctx) 105 | if err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | func (repo *ProductRepository) UpdateProductByPK(ctx context.Context, product *entities.Product) error { 112 | if repo.database == nil { 113 | return cerrors.ErrInvalidDatabaseClient 114 | } 115 | 116 | product.UpdatedAt = time.Now() 117 | _, err := repo.database.NewUpdate().Model(product).WherePK().Exec(ctx) 118 | if err != nil { 119 | return err 120 | } 121 | return nil 122 | } 123 | 124 | func (repo *ProductRepository) InsertNewProductToken(ctx context.Context, productToken *entities.ProductToken) error { 125 | if repo.database == nil { 126 | return cerrors.ErrInvalidDatabaseClient 127 | } 128 | 129 | _, err := repo.database.NewInsert().Model(productToken).Exec(ctx) 130 | if err != nil { 131 | return err 132 | } 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /internal/utils/rsa2048_pkcs1.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha512" 8 | "crypto/x509" 9 | "encoding/base64" 10 | "encoding/json" 11 | "encoding/pem" 12 | "errors" 13 | "fmt" 14 | "strings" 15 | ) 16 | 17 | const RSAPrivateKeyStr = "RSA PRIVATE KEY" 18 | const RSAPublicKeyStr = "RSA PUBLIC KEY" 19 | 20 | // NewRSA2048PKCS1KeyPair generates the private key and the public key pair using RSA2048 algorithm 21 | // Return te signingKey (private key) and verifyKey (public key) 22 | func NewRSA2048PKCS1KeyPair() (string, string, error) { 23 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 24 | if err != nil { 25 | return "", "", err 26 | } 27 | 28 | // Encode the private key to PEM format (PKCS1) 29 | privateKeyPEM := pem.EncodeToMemory(&pem.Block{ 30 | Type: RSAPrivateKeyStr, 31 | Bytes: x509.MarshalPKCS1PrivateKey(privateKey), 32 | }) 33 | 34 | // Encode the public key to PEM format (PKCS1) 35 | publicKeyPEM := pem.EncodeToMemory(&pem.Block{ 36 | Type: RSAPublicKeyStr, 37 | Bytes: x509.MarshalPKCS1PublicKey(&privateKey.PublicKey), 38 | }) 39 | 40 | return base64.StdEncoding.EncodeToString(privateKeyPEM), base64.StdEncoding.EncodeToString(publicKeyPEM), nil 41 | } 42 | 43 | // NewLicenseKeyWithRSA2048PKCS1 generates new license key using RSA2048 algorithm 44 | // // Returns a license string in format {{signature}}.{{data}} 45 | func NewLicenseKeyWithRSA2048PKCS1(signingKey string, data any) (string, error) { 46 | bData, err := json.Marshal(data) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | // Encode the original data to base64 52 | encodedData := base64.StdEncoding.EncodeToString(bData) 53 | 54 | // Sign the data using the private key with SHA-512 hashing 55 | hash := sha512.New() 56 | hash.Write(bData) 57 | hashed := hash.Sum(nil) 58 | 59 | // Decode the private key string 60 | privateKeyPEM, err := base64.StdEncoding.DecodeString(signingKey) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | block, _ := pem.Decode(privateKeyPEM) 66 | 67 | if block == nil || block.Type != RSAPrivateKeyStr { 68 | return "", errors.New("failed to decode PEM block containing private key") 69 | } 70 | 71 | privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA512, hashed) 77 | if err != nil { 78 | return "", err 79 | } 80 | 81 | // Encode the signature in base64 82 | encodedSignature := base64.StdEncoding.EncodeToString(signature) 83 | 84 | // Combine the encoded data and signature to create the license key 85 | licenseKey := fmt.Sprintf("%s.%s", encodedSignature, encodedData) 86 | 87 | return licenseKey, nil 88 | } 89 | 90 | // VerifyLicenseKeyWithRSA2048PKCS1 verifies a license key against the provided public key using Ed25519 algorithm 91 | func VerifyLicenseKeyWithRSA2048PKCS1(verifyKey string, licenseKey string) (bool, []byte, error) { 92 | parts := strings.Split(licenseKey, ".") 93 | if len(parts) != 2 { 94 | return false, nil, errors.New("invalid license key format") 95 | } 96 | encodedSignature := parts[0] 97 | encodedData := parts[1] 98 | 99 | data, err := base64.StdEncoding.DecodeString(encodedData) 100 | if err != nil { 101 | return false, nil, err 102 | } 103 | 104 | // Decode signature 105 | signature, err := base64.StdEncoding.DecodeString(encodedSignature) 106 | if err != nil { 107 | return false, nil, err 108 | } 109 | 110 | // Decode the public key string 111 | publicKeyPEM, err := base64.StdEncoding.DecodeString(verifyKey) 112 | if err != nil { 113 | return false, nil, err 114 | } 115 | 116 | block, _ := pem.Decode(publicKeyPEM) 117 | 118 | if block == nil || block.Type != RSAPublicKeyStr { 119 | return false, nil, errors.New("failed to decode PEM block containing public key") 120 | } 121 | 122 | publicKey, err := x509.ParsePKCS1PublicKey(block.Bytes) 123 | if err != nil { 124 | return false, nil, err 125 | } 126 | 127 | // Check sum of data 128 | hashed := sha512.Sum512(data) 129 | err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA512, hashed[:], signature) 130 | if err != nil { 131 | return false, nil, err 132 | } 133 | 134 | return true, data, nil 135 | } 136 | -------------------------------------------------------------------------------- /internal/services/v1/accounts/models/account_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/constants" 6 | "go-license-management/internal/infrastructure/models/account_attribute" 7 | "go.opentelemetry.io/otel/trace" 8 | "time" 9 | ) 10 | 11 | type AccountRegistrationInput struct { 12 | TracerCtx context.Context 13 | Tracer trace.Tracer 14 | account_attribute.AccountCommonURI 15 | Username *string `json:"username" validate:"required" example:"test"` 16 | Password *string `json:"password" validate:"required" example:"test"` 17 | FirstName *string `json:"first_name" validate:"required" example:"test"` 18 | LastName *string `json:"lastName" validate:"required" example:"test"` 19 | Email *string `json:"email" validate:"required" example:"test"` 20 | Role *string `json:"role" validate:"required" example:"test"` 21 | Metadata map[string]interface{} `json:"metadata" validate:"required" example:"test"` 22 | } 23 | 24 | type AccountRegistrationOutput struct { 25 | Username string `json:"username"` 26 | RoleName string `json:"role_name"` 27 | Email string `json:"email"` 28 | FirstName string `json:"first_name"` 29 | LastName string `json:"last_name"` 30 | Status string `json:"status"` 31 | Metadata map[string]interface{} `json:"metadata"` 32 | CreatedAt time.Time `json:"created_at"` 33 | UpdatedAt time.Time `json:"updated_at"` 34 | } 35 | 36 | type AccountRetrievalOutput struct { 37 | Username string `json:"username"` 38 | RoleName string `json:"role_name"` 39 | Email string `json:"email"` 40 | FirstName string `json:"first_name"` 41 | LastName string `json:"last_name"` 42 | Status string `json:"status"` 43 | Metadata map[string]interface{} `json:"metadata"` 44 | CreatedAt time.Time `json:"created_at"` 45 | UpdatedAt time.Time `json:"updated_at"` 46 | } 47 | 48 | type AccountRetrievalInput struct { 49 | TracerCtx context.Context 50 | Tracer trace.Tracer 51 | account_attribute.AccountCommonURI 52 | } 53 | 54 | type AccountDeletionInput struct { 55 | TracerCtx context.Context 56 | Tracer trace.Tracer 57 | account_attribute.AccountCommonURI 58 | } 59 | 60 | type AccountUpdateInput struct { 61 | TracerCtx context.Context 62 | Tracer trace.Tracer 63 | account_attribute.AccountCommonURI 64 | Password *string `json:"password" validate:"required" example:"test"` 65 | FirstName *string `json:"first_name" validate:"optional" example:"test"` 66 | LastName *string `json:"lastName" validate:"optional" example:"test"` 67 | Email *string `json:"email" validate:"required" example:"test"` 68 | Role *string `json:"role" validate:"required" example:"test"` 69 | Metadata map[string]interface{} `json:"metadata" validate:"optional" example:"test"` 70 | } 71 | 72 | type AccountUpdateOutput struct { 73 | Username string `json:"username"` 74 | RoleName string `json:"role_name"` 75 | Email string `json:"email"` 76 | FirstName string `json:"first_name"` 77 | LastName string `json:"last_name"` 78 | Status string `json:"status"` 79 | Metadata map[string]interface{} `json:"metadata"` 80 | CreatedAt time.Time `json:"created_at"` 81 | UpdatedAt time.Time `json:"updated_at"` 82 | } 83 | 84 | type AccountPasswordTokenOutput struct { 85 | Token string `json:"token"` 86 | } 87 | 88 | type AccountListInput struct { 89 | TracerCtx context.Context 90 | Tracer trace.Tracer 91 | account_attribute.AccountCommonURI 92 | constants.QueryCommonParam 93 | } 94 | 95 | type AccountActionInput struct { 96 | TracerCtx context.Context 97 | Tracer trace.Tracer 98 | NewPassword *string `json:"new_password"` 99 | CurrentPassword *string `json:"current_password"` 100 | ResetToken *string `json:"reset_token"` 101 | account_attribute.AccountCommonURI 102 | } 103 | 104 | type AccountActionGenerateResetTokenOutput struct { 105 | ResetToken string `json:"reset_token"` 106 | } 107 | -------------------------------------------------------------------------------- /internal/middlewares/machine_action_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "fmt" 5 | "github.com/casbin/casbin/v2" 6 | "github.com/gin-gonic/gin" 7 | "go-license-management/internal/cerrors" 8 | "go-license-management/internal/constants" 9 | "go-license-management/internal/infrastructure/casbin_adapter" 10 | "go-license-management/internal/infrastructure/logging" 11 | "go-license-management/internal/permissions" 12 | "go-license-management/internal/response" 13 | "net/http" 14 | "strings" 15 | ) 16 | 17 | func MachineActionPermissionValidationMW() gin.HandlerFunc { 18 | return func(ctx *gin.Context) { 19 | actions := ctx.Param("machine_action") 20 | if actions == "" { 21 | ctx.AbortWithStatusJSON( 22 | http.StatusUnauthorized, 23 | response.NewResponse(ctx).ToResponse( 24 | cerrors.ErrCodeMapper[cerrors.ErrGenericUnauthorized], 25 | "missing authorization header", 26 | nil, 27 | nil, 28 | nil, 29 | ), 30 | ) 31 | return 32 | } 33 | 34 | e, err := casbin.NewEnforcer(casbin_adapter.GetEnforcerModel(), casbin_adapter.GetAdapter()) 35 | if err != nil { 36 | logging.GetInstance().GetLogger().Error(err.Error()) 37 | ctx.AbortWithStatusJSON( 38 | http.StatusInternalServerError, 39 | response.NewResponse(ctx).ToResponse( 40 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 41 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 42 | nil, 43 | nil, 44 | nil, 45 | ), 46 | ) 47 | return 48 | } 49 | 50 | err = e.LoadPolicy() 51 | if err != nil { 52 | logging.GetInstance().GetLogger().Error(err.Error()) 53 | ctx.AbortWithStatusJSON( 54 | http.StatusInternalServerError, 55 | response.NewResponse(ctx).ToResponse( 56 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 57 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 58 | nil, 59 | nil, 60 | nil, 61 | ), 62 | ) 63 | return 64 | } 65 | 66 | var permission string 67 | 68 | switch actions { 69 | case constants.MachineActionCheckout: 70 | permission = permissions.MachineCheckOut 71 | case constants.MachineActionPingHeartbeat: 72 | permission = permissions.MachineHeartbeatPing 73 | case constants.MachineActionResetHeartbeat: 74 | permission = permissions.MachineHeartbeatReset 75 | default: 76 | ctx.AbortWithStatusJSON( 77 | http.StatusBadRequest, 78 | response.NewResponse(ctx).ToResponse( 79 | cerrors.ErrCodeMapper[cerrors.ErrAccountActionIsInvalid], 80 | cerrors.ErrMessageMapper[cerrors.ErrAccountActionIsInvalid], 81 | nil, 82 | nil, 83 | nil, 84 | ), 85 | ) 86 | return 87 | 88 | } 89 | permObjects := strings.Split(permission, ".") 90 | 91 | ok, err := e.Enforce( 92 | ctx.GetString(constants.ContextValueTenant), 93 | ctx.GetString(constants.ContextValueSubject), 94 | permObjects[0], 95 | permObjects[1], 96 | ) 97 | if err != nil { 98 | logging.GetInstance().GetLogger().Error(err.Error()) 99 | ctx.AbortWithStatusJSON( 100 | http.StatusInternalServerError, 101 | response.NewResponse(ctx).ToResponse( 102 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 103 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 104 | nil, 105 | nil, 106 | nil, 107 | ), 108 | ) 109 | return 110 | } 111 | 112 | if !ok { 113 | logging.GetInstance().GetLogger().Info( 114 | fmt.Sprintf("invalid permission: domain [%s] | subject [%s] | object [%s] | action [%s]", 115 | ctx.GetString(constants.ContextValueTenant), 116 | ctx.GetString(constants.ContextValueSubject), 117 | permObjects[0], 118 | permObjects[1]), 119 | ) 120 | ctx.AbortWithStatusJSON( 121 | http.StatusForbidden, 122 | response.NewResponse(ctx).ToResponse( 123 | cerrors.ErrCodeMapper[cerrors.ErrGenericPermission], 124 | fmt.Sprintf("user [%s] does not have permission to perform the requested action", ctx.GetString(constants.ContextValueSubject)), 125 | nil, 126 | nil, 127 | nil, 128 | ), 129 | ) 130 | return 131 | } 132 | logging.GetInstance().GetLogger().Info( 133 | fmt.Sprintf("valid permission: domain [%s] | subject [%s] | object [%s] | action [%s]", 134 | ctx.GetString(constants.ContextValueTenant), 135 | ctx.GetString(constants.ContextValueSubject), 136 | permObjects[0], 137 | permObjects[1]), 138 | ) 139 | 140 | ctx.Next() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /internal/repositories/v1/accounts/repository.go: -------------------------------------------------------------------------------- 1 | package accounts 2 | 3 | import ( 4 | "context" 5 | "github.com/uptrace/bun" 6 | "go-license-management/internal/cerrors" 7 | "go-license-management/internal/constants" 8 | "go-license-management/internal/infrastructure/database/entities" 9 | "go-license-management/internal/utils" 10 | "go-license-management/server/api" 11 | "time" 12 | ) 13 | 14 | type AccountRepository struct { 15 | database *bun.DB 16 | } 17 | 18 | func NewAccountRepository(ds *api.DataSource) *AccountRepository { 19 | return &AccountRepository{ 20 | database: ds.GetDatabase(), 21 | } 22 | } 23 | 24 | func (repo *AccountRepository) SelectTenantByPK(ctx context.Context, tenantName string) (*entities.Tenant, error) { 25 | if repo.database == nil { 26 | return nil, cerrors.ErrInvalidDatabaseClient 27 | } 28 | 29 | tenant := &entities.Tenant{Name: tenantName} 30 | 31 | err := repo.database.NewSelect().Model(tenant).WherePK().Scan(ctx) 32 | if err != nil { 33 | return tenant, err 34 | } 35 | 36 | return tenant, nil 37 | } 38 | 39 | func (repo *AccountRepository) SelectAccountsByTenant(ctx context.Context, tenantName string, queryParam constants.QueryCommonParam) ([]entities.Account, int, error) { 40 | var count = 0 41 | if repo.database == nil { 42 | return nil, count, cerrors.ErrInvalidDatabaseClient 43 | } 44 | 45 | accounts := make([]entities.Account, 0) 46 | count, err := repo.database.NewSelect().Model(new(entities.Account)). 47 | Where("tenant_name = ?", tenantName). 48 | Order("created_at DESC"). 49 | Limit(utils.DerefPointer(queryParam.Limit)). 50 | Offset(utils.DerefPointer(queryParam.Offset)). 51 | ScanAndCount(ctx, &accounts) 52 | if err != nil { 53 | return accounts, count, nil 54 | } 55 | 56 | return accounts, count, nil 57 | } 58 | 59 | func (repo *AccountRepository) SelectAccountByPK(ctx context.Context, tenantName, username string) (*entities.Account, error) { 60 | if repo.database == nil { 61 | return nil, cerrors.ErrInvalidDatabaseClient 62 | } 63 | 64 | account := &entities.Account{Username: username, TenantName: tenantName} 65 | err := repo.database.NewSelect().Model(account).WherePK().Scan(ctx) 66 | if err != nil { 67 | return account, err 68 | } 69 | return account, nil 70 | } 71 | 72 | func (repo *AccountRepository) UpdateAccountByPK(ctx context.Context, account *entities.Account) (*entities.Account, error) { 73 | if repo.database == nil { 74 | return account, cerrors.ErrInvalidDatabaseClient 75 | } 76 | 77 | account.UpdatedAt = time.Now() 78 | _, err := repo.database.NewUpdate().Model(account).WherePK().Exec(ctx) 79 | if err != nil { 80 | return account, err 81 | } 82 | return account, nil 83 | } 84 | 85 | func (repo *AccountRepository) CheckAccountExistByPK(ctx context.Context, tenantName, username string) (bool, error) { 86 | if repo.database == nil { 87 | return false, cerrors.ErrInvalidDatabaseClient 88 | } 89 | 90 | account := &entities.Account{Username: username, TenantName: tenantName} 91 | exist, err := repo.database.NewSelect().Model(account).WherePK().Exists(ctx) 92 | if err != nil { 93 | return exist, err 94 | } 95 | return exist, nil 96 | } 97 | 98 | func (repo *AccountRepository) CheckAccountEmailExistByPK(ctx context.Context, tenantName, email string) (bool, error) { 99 | if repo.database == nil { 100 | return false, cerrors.ErrInvalidDatabaseClient 101 | } 102 | 103 | account := &entities.Account{Email: email, TenantName: tenantName} 104 | exist, err := repo.database.NewSelect().Model(account).Where("tenant_name = ? AND email = ?", tenantName, email).Exists(ctx) 105 | if err != nil { 106 | return exist, err 107 | } 108 | return exist, nil 109 | } 110 | 111 | func (repo *AccountRepository) InsertNewAccount(ctx context.Context, account *entities.Account) error { 112 | if repo.database == nil { 113 | return cerrors.ErrInvalidDatabaseClient 114 | } 115 | 116 | _, err := repo.database.NewInsert().Model(account).Exec(ctx) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (repo *AccountRepository) DeleteAccountByPK(ctx context.Context, tenantName, username string) error { 125 | if repo.database == nil { 126 | return cerrors.ErrInvalidDatabaseClient 127 | } 128 | 129 | account := &entities.Account{Username: username, TenantName: tenantName} 130 | _, err := repo.database.NewDelete().Model(account).WherePK().Exec(ctx) 131 | if err != nil { 132 | return err 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /internal/constants/policy_constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // PolicySchemeED25519 signs license keys with your account's 5 | // Ed25519 signing key, 6 | PolicySchemeED25519 = "ED25519" 7 | 8 | // PolicySchemeRSA2048PKCS1 signs license keys with your account's 9 | // 2048-bit RSA private key using RSA PKCS1 v1.5 padding 10 | PolicySchemeRSA2048PKCS1 = "RSA2048PKCS1" 11 | ) 12 | 13 | var ValidPolicySchemeMapper = map[string]bool{ 14 | PolicySchemeED25519: true, 15 | PolicySchemeRSA2048PKCS1: true, 16 | } 17 | 18 | const ( 19 | // PolicyExpirationStrategyRestrictAccess - expired licenses can 20 | // continue to access releases published prior to 21 | // their license expiry. This is the default. 22 | PolicyExpirationStrategyRestrictAccess = "restrict" 23 | 24 | // PolicyExpirationStrategyRevokeAccess - Expired licenses are 25 | // no longer able to access any releases. 26 | PolicyExpirationStrategyRevokeAccess = "revoke" 27 | 28 | // PolicyExpirationStrategyMaintainAccess - Expired licenses can continue 29 | // to access releases published prior to their license expiry. 30 | PolicyExpirationStrategyMaintainAccess = "maintain" 31 | 32 | // PolicyExpirationStrategyAllowAccess - Expired licenses can access any releases. 33 | PolicyExpirationStrategyAllowAccess = "allow" 34 | ) 35 | 36 | var ValidPolicyExpirationStrategyMapper = map[string]bool{ 37 | PolicyExpirationStrategyRestrictAccess: true, 38 | PolicyExpirationStrategyRevokeAccess: true, 39 | PolicyExpirationStrategyMaintainAccess: true, 40 | PolicyExpirationStrategyAllowAccess: true, 41 | } 42 | 43 | const ( 44 | // PolicyCheckinIntervalDaily requires a license implementing 45 | //the policy checkin at least once every day to remain valid. 46 | PolicyCheckinIntervalDaily = "daily" 47 | // PolicyCheckinIntervalWeekly requires a license implementing 48 | // the policy checkin at least once every week to remain valid. 49 | PolicyCheckinIntervalWeekly = "weekly" 50 | // PolicyCheckinIntervalMonthly requires a license implementing 51 | // the policy checkin at least once every month to remain valid. 52 | PolicyCheckinIntervalMonthly = "monthly" 53 | // PolicyCheckinIntervalYearly requires a license implementing 54 | // the policy to check in at least once every year to remain valid. 55 | PolicyCheckinIntervalYearly = "yearly" 56 | ) 57 | 58 | var ValidPolicyCheckinIntervalMapper = map[string]bool{ 59 | PolicyCheckinIntervalDaily: true, 60 | PolicyCheckinIntervalWeekly: true, 61 | PolicyCheckinIntervalMonthly: true, 62 | PolicyCheckinIntervalYearly: true, 63 | } 64 | 65 | const ( 66 | // PolicyRenewalBasisFromExpiry - License expiry is extended from the license's current expiry value, 67 | // i.e. license.expiry = license.expiry + policy.duration. This is the default. 68 | PolicyRenewalBasisFromExpiry = "from_expiry" 69 | // PolicyRenewalFromNow - License expiry is extended from the current time, 70 | // i.e. license.expiry = time.now + policy.duration. 71 | PolicyRenewalFromNow = "from_now" 72 | // PolicyRenewalFromNowIfExpired - Conditionally extend license expiry from 73 | // the current time if the license is expired, otherwise extend from the license's current expiry value. 74 | PolicyRenewalFromNowIfExpired = "from_now_if_expired" 75 | ) 76 | 77 | var ValidPolicyRenewalBasisMapper = map[string]bool{ 78 | PolicyRenewalBasisFromExpiry: true, 79 | PolicyRenewalFromNow: true, 80 | PolicyRenewalFromNowIfExpired: true, 81 | } 82 | 83 | const ( 84 | // PolicyHeartbeatBasisFromCreation - Machine heartbeat is started immediately upon creation. 85 | PolicyHeartbeatBasisFromCreation = "from_creation" 86 | 87 | // PolicyHeartbeatBasisFromFirstPing - Machine heartbeat is started after their first heartbeat ping event. 88 | PolicyHeartbeatBasisFromFirstPing = "from_first_ping" 89 | ) 90 | 91 | var ValidPolicyHeartbeatBasisMapper = map[string]bool{ 92 | PolicyHeartbeatBasisFromCreation: true, 93 | PolicyHeartbeatBasisFromFirstPing: true, 94 | } 95 | 96 | const ( 97 | // PolicyOverageStrategyNoOverage - Do not allow overages. Attempts to exceed limits will fail. This is the default. 98 | PolicyOverageStrategyNoOverage = "no_overage" 99 | // PolicyOverageStrategyAlwaysAllow - The license may exceed its limits, and doing so will not affect the license validity. 100 | PolicyOverageStrategyAlwaysAllow = "always_allow" 101 | ) 102 | 103 | var ValidPolicyOverageStrategyMapper = map[string]bool{ 104 | PolicyOverageStrategyNoOverage: true, 105 | PolicyOverageStrategyAlwaysAllow: true, 106 | } 107 | -------------------------------------------------------------------------------- /internal/middlewares/account_action_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "fmt" 5 | "github.com/casbin/casbin/v2" 6 | "github.com/gin-gonic/gin" 7 | "go-license-management/internal/cerrors" 8 | "go-license-management/internal/constants" 9 | "go-license-management/internal/infrastructure/casbin_adapter" 10 | "go-license-management/internal/infrastructure/logging" 11 | "go-license-management/internal/permissions" 12 | "go-license-management/internal/response" 13 | "net/http" 14 | "strings" 15 | ) 16 | 17 | func AccountActionPermissionValidationMW() gin.HandlerFunc { 18 | return func(ctx *gin.Context) { 19 | actions := ctx.Param("action") 20 | if actions == "" { 21 | ctx.AbortWithStatusJSON( 22 | http.StatusUnauthorized, 23 | response.NewResponse(ctx).ToResponse( 24 | cerrors.ErrCodeMapper[cerrors.ErrGenericUnauthorized], 25 | "missing authorization header", 26 | nil, 27 | nil, 28 | nil, 29 | ), 30 | ) 31 | return 32 | } 33 | 34 | e, err := casbin.NewEnforcer(casbin_adapter.GetEnforcerModel(), casbin_adapter.GetAdapter()) 35 | if err != nil { 36 | logging.GetInstance().GetLogger().Error(err.Error()) 37 | ctx.AbortWithStatusJSON( 38 | http.StatusInternalServerError, 39 | response.NewResponse(ctx).ToResponse( 40 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 41 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 42 | nil, 43 | nil, 44 | nil, 45 | ), 46 | ) 47 | return 48 | } 49 | 50 | err = e.LoadPolicy() 51 | if err != nil { 52 | logging.GetInstance().GetLogger().Error(err.Error()) 53 | ctx.AbortWithStatusJSON( 54 | http.StatusInternalServerError, 55 | response.NewResponse(ctx).ToResponse( 56 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 57 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 58 | nil, 59 | nil, 60 | nil, 61 | ), 62 | ) 63 | return 64 | } 65 | 66 | var permission string 67 | 68 | switch actions { 69 | case constants.AccountActionBan: 70 | permission = permissions.UserBan 71 | case constants.AccountActionUnban: 72 | permission = permissions.UserUnban 73 | case constants.AccountActionUpdatePassword: 74 | permission = permissions.UserPasswordUpdate 75 | case constants.AccountActionResetPassword, constants.AccountActionGenerateResetToken: 76 | permission = permissions.UserPasswordReset 77 | default: 78 | ctx.AbortWithStatusJSON( 79 | http.StatusBadRequest, 80 | response.NewResponse(ctx).ToResponse( 81 | cerrors.ErrCodeMapper[cerrors.ErrAccountActionIsInvalid], 82 | cerrors.ErrMessageMapper[cerrors.ErrAccountActionIsInvalid], 83 | nil, 84 | nil, 85 | nil, 86 | ), 87 | ) 88 | return 89 | 90 | } 91 | permObjects := strings.Split(permission, ".") 92 | 93 | ok, err := e.Enforce( 94 | ctx.GetString(constants.ContextValueTenant), 95 | ctx.GetString(constants.ContextValueSubject), 96 | permObjects[0], 97 | permObjects[1], 98 | ) 99 | if err != nil { 100 | logging.GetInstance().GetLogger().Error(err.Error()) 101 | ctx.AbortWithStatusJSON( 102 | http.StatusInternalServerError, 103 | response.NewResponse(ctx).ToResponse( 104 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 105 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 106 | nil, 107 | nil, 108 | nil, 109 | ), 110 | ) 111 | return 112 | } 113 | 114 | if !ok { 115 | logging.GetInstance().GetLogger().Info( 116 | fmt.Sprintf("invalid permission: domain [%s] | subject [%s] | object [%s] | action [%s]", 117 | ctx.GetString(constants.ContextValueTenant), 118 | ctx.GetString(constants.ContextValueSubject), 119 | permObjects[0], 120 | permObjects[1]), 121 | ) 122 | ctx.AbortWithStatusJSON( 123 | http.StatusForbidden, 124 | response.NewResponse(ctx).ToResponse( 125 | cerrors.ErrCodeMapper[cerrors.ErrGenericPermission], 126 | fmt.Sprintf("user [%s] does not have permission to perform the requested action", ctx.GetString(constants.ContextValueSubject)), 127 | nil, 128 | nil, 129 | nil, 130 | ), 131 | ) 132 | return 133 | } 134 | logging.GetInstance().GetLogger().Info( 135 | fmt.Sprintf("valid permission: domain [%s] | subject [%s] | object [%s] | action [%s]", 136 | ctx.GetString(constants.ContextValueTenant), 137 | ctx.GetString(constants.ContextValueSubject), 138 | permObjects[0], 139 | permObjects[1]), 140 | ) 141 | 142 | ctx.Next() 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /server/api/v1/machines/machine_model.go: -------------------------------------------------------------------------------- 1 | package machines 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/models/machine_attribute" 8 | "go-license-management/internal/services/v1/machines/models" 9 | "go-license-management/internal/utils" 10 | "go.opentelemetry.io/otel/trace" 11 | ) 12 | 13 | type MachineRegistrationRequest struct { 14 | machine_attribute.MachineAttributeModel 15 | } 16 | 17 | func (req *MachineRegistrationRequest) Validate() error { 18 | if req.Fingerprint == nil { 19 | return cerrors.ErrMachineFingerprintIsEmpty 20 | } 21 | 22 | if req.LicenseKey == nil { 23 | return cerrors.ErrMachineLicenseIsEmpty 24 | } else { 25 | if len(utils.DerefPointer(req.LicenseKey)) != 44 { 26 | return cerrors.ErrMachineLicenseIsInvalid 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | func (req *MachineRegistrationRequest) ToMachineRegistrationInput(ctx context.Context, tracer trace.Tracer, machineURI machine_attribute.MachineCommonURI) *models.MachineRegistrationInput { 33 | return &models.MachineRegistrationInput{ 34 | TracerCtx: ctx, 35 | Tracer: tracer, 36 | MachineCommonURI: machineURI, 37 | MachineAttributeModel: req.MachineAttributeModel, 38 | } 39 | } 40 | 41 | type MachineUpdateRequest struct { 42 | machine_attribute.MachineAttributeModel 43 | } 44 | 45 | func (req *MachineUpdateRequest) Validate() error { 46 | if req.LicenseKey != nil { 47 | if len(utils.DerefPointer(req.LicenseKey)) != 44 { 48 | return cerrors.ErrMachineLicenseIsInvalid 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | func (req *MachineUpdateRequest) ToMachineUpdateInput(ctx context.Context, tracer trace.Tracer, machineURI machine_attribute.MachineCommonURI) *models.MachineUpdateInput { 55 | 56 | return &models.MachineUpdateInput{ 57 | TracerCtx: ctx, 58 | Tracer: tracer, 59 | MachineCommonURI: machineURI, 60 | MachineAttributeModel: req.MachineAttributeModel, 61 | } 62 | } 63 | 64 | type MachineRetrievalRequest struct { 65 | machine_attribute.MachineCommonURI 66 | } 67 | 68 | func (req *MachineRetrievalRequest) Validate() error { 69 | if req.MachineID == nil { 70 | return cerrors.ErrMachineIDIsEmpty 71 | } 72 | return req.MachineCommonURI.Validate() 73 | } 74 | 75 | func (req *MachineRetrievalRequest) ToMachineRetrievalInput(ctx context.Context, tracer trace.Tracer) *models.MachineRetrievalInput { 76 | return &models.MachineRetrievalInput{ 77 | TracerCtx: ctx, 78 | Tracer: tracer, 79 | MachineCommonURI: req.MachineCommonURI, 80 | } 81 | 82 | } 83 | 84 | type MachineListRequest struct { 85 | constants.QueryCommonParam 86 | } 87 | 88 | func (req *MachineListRequest) Validate() error { 89 | req.QueryCommonParam.Validate() 90 | return nil 91 | } 92 | 93 | func (req *MachineListRequest) ToMachineListInput(ctx context.Context, tracer trace.Tracer, machineURI machine_attribute.MachineCommonURI) *models.MachineListInput { 94 | return &models.MachineListInput{ 95 | TracerCtx: ctx, 96 | Tracer: tracer, 97 | MachineCommonURI: machineURI, 98 | QueryCommonParam: req.QueryCommonParam, 99 | } 100 | } 101 | 102 | type MachineDeletionRequest struct { 103 | machine_attribute.MachineCommonURI 104 | } 105 | 106 | func (req *MachineDeletionRequest) Validate() error { 107 | if req.MachineID == nil { 108 | return cerrors.ErrMachineIDIsEmpty 109 | } 110 | return req.MachineCommonURI.Validate() 111 | } 112 | 113 | func (req *MachineDeletionRequest) ToMachineDeletionInput(ctx context.Context, tracer trace.Tracer) *models.MachineDeleteInput { 114 | return &models.MachineDeleteInput{ 115 | TracerCtx: ctx, 116 | Tracer: tracer, 117 | MachineCommonURI: req.MachineCommonURI, 118 | } 119 | } 120 | 121 | type MachineDeactivateRequest struct{} 122 | 123 | func (req *MachineDeactivateRequest) Validate() error { 124 | return nil 125 | } 126 | 127 | type MachineHeartbeatRequest struct{} 128 | 129 | func (req *MachineHeartbeatRequest) Validate() error { 130 | return nil 131 | } 132 | 133 | type MachineActionsRequest struct { 134 | machine_attribute.MachineCommonURI 135 | } 136 | 137 | func (req *MachineActionsRequest) Validate() error { 138 | if req.MachineAction == nil { 139 | return cerrors.ErrMachineActionIsEmpty 140 | } 141 | 142 | return req.MachineCommonURI.Validate() 143 | } 144 | 145 | func (req *MachineActionsRequest) ToMachineActionsInput(ctx context.Context, tracer trace.Tracer, query machine_attribute.MachineActionsQueryParam) *models.MachineActionsInput { 146 | return &models.MachineActionsInput{ 147 | TracerCtx: ctx, 148 | Tracer: tracer, 149 | MachineCommonURI: req.MachineCommonURI, 150 | MachineActionsQueryParam: query, 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /internal/middlewares/license_action_mw.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "fmt" 5 | "github.com/casbin/casbin/v2" 6 | "github.com/gin-gonic/gin" 7 | "go-license-management/internal/cerrors" 8 | "go-license-management/internal/constants" 9 | "go-license-management/internal/infrastructure/casbin_adapter" 10 | "go-license-management/internal/infrastructure/logging" 11 | "go-license-management/internal/permissions" 12 | "go-license-management/internal/response" 13 | "net/http" 14 | "strings" 15 | ) 16 | 17 | func LicenseActionPermissionValidationMW() gin.HandlerFunc { 18 | return func(ctx *gin.Context) { 19 | actions := ctx.Param("action") 20 | if actions == "" { 21 | ctx.AbortWithStatusJSON( 22 | http.StatusUnauthorized, 23 | response.NewResponse(ctx).ToResponse( 24 | cerrors.ErrCodeMapper[cerrors.ErrGenericUnauthorized], 25 | "missing authorization header", 26 | nil, 27 | nil, 28 | nil, 29 | ), 30 | ) 31 | return 32 | } 33 | 34 | e, err := casbin.NewEnforcer(casbin_adapter.GetEnforcerModel(), casbin_adapter.GetAdapter()) 35 | if err != nil { 36 | logging.GetInstance().GetLogger().Error(err.Error()) 37 | ctx.AbortWithStatusJSON( 38 | http.StatusInternalServerError, 39 | response.NewResponse(ctx).ToResponse( 40 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 41 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 42 | nil, 43 | nil, 44 | nil, 45 | ), 46 | ) 47 | return 48 | } 49 | 50 | err = e.LoadPolicy() 51 | if err != nil { 52 | logging.GetInstance().GetLogger().Error(err.Error()) 53 | ctx.AbortWithStatusJSON( 54 | http.StatusInternalServerError, 55 | response.NewResponse(ctx).ToResponse( 56 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 57 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 58 | nil, 59 | nil, 60 | nil, 61 | ), 62 | ) 63 | return 64 | } 65 | 66 | var permission string 67 | 68 | switch actions { 69 | case constants.LicenseActionCheckin: 70 | permission = permissions.LicenseCheckIn 71 | case constants.LicenseActionCheckout: 72 | permission = permissions.LicenseCheckOut 73 | case constants.LicenseActionValidate: 74 | permission = permissions.LicenseValidate 75 | case constants.LicenseActionIncrementUsage: 76 | permission = permissions.LicenseUsageIncrement 77 | case constants.LicenseActionDecrementUsage: 78 | permission = permissions.LicenseUsageDecrement 79 | case constants.LicenseActionRenew: 80 | permission = permissions.LicenseRenew 81 | case constants.LicenseActionReinstate: 82 | permission = permissions.LicenseReinstate 83 | case constants.LicenseActionResetUsage: 84 | permission = permissions.LicenseUsageReset 85 | case constants.LicenseActionSuspend: 86 | permission = permissions.LicenseSuspend 87 | default: 88 | ctx.AbortWithStatusJSON( 89 | http.StatusBadRequest, 90 | response.NewResponse(ctx).ToResponse( 91 | cerrors.ErrCodeMapper[cerrors.ErrLicenseActionIsInvalid], 92 | cerrors.ErrMessageMapper[cerrors.ErrLicenseActionIsInvalid], 93 | nil, 94 | nil, 95 | nil, 96 | ), 97 | ) 98 | return 99 | 100 | } 101 | permObjects := strings.Split(permission, ".") 102 | 103 | ok, err := e.Enforce( 104 | ctx.GetString(constants.ContextValueTenant), 105 | ctx.GetString(constants.ContextValueSubject), 106 | permObjects[0], 107 | permObjects[1], 108 | ) 109 | if err != nil { 110 | logging.GetInstance().GetLogger().Error(err.Error()) 111 | ctx.AbortWithStatusJSON( 112 | http.StatusInternalServerError, 113 | response.NewResponse(ctx).ToResponse( 114 | cerrors.ErrCodeMapper[cerrors.ErrGenericInternalServer], 115 | cerrors.ErrMessageMapper[cerrors.ErrGenericInternalServer], 116 | nil, 117 | nil, 118 | nil, 119 | ), 120 | ) 121 | return 122 | } 123 | 124 | if !ok { 125 | logging.GetInstance().GetLogger().Info( 126 | fmt.Sprintf("invalid permission: domain [%s] | subject [%s] | object [%s] | action [%s]", 127 | ctx.GetString(constants.ContextValueTenant), 128 | ctx.GetString(constants.ContextValueSubject), 129 | permObjects[0], 130 | permObjects[1]), 131 | ) 132 | ctx.AbortWithStatusJSON( 133 | http.StatusForbidden, 134 | response.NewResponse(ctx).ToResponse( 135 | cerrors.ErrCodeMapper[cerrors.ErrGenericPermission], 136 | fmt.Sprintf("user [%s] does not have permission to perform the requested action", ctx.GetString(constants.ContextValueSubject)), 137 | nil, 138 | nil, 139 | nil, 140 | ), 141 | ) 142 | return 143 | } 144 | logging.GetInstance().GetLogger().Info( 145 | fmt.Sprintf("valid permission: domain [%s] | subject [%s] | object [%s] | action [%s]", 146 | ctx.GetString(constants.ContextValueTenant), 147 | ctx.GetString(constants.ContextValueSubject), 148 | permObjects[0], 149 | permObjects[1]), 150 | ) 151 | 152 | ctx.Next() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /server/api/v1/products/product_model.go: -------------------------------------------------------------------------------- 1 | package products 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/cerrors" 6 | "go-license-management/internal/constants" 7 | "go-license-management/internal/infrastructure/models/product_attribute" 8 | "go-license-management/internal/services/v1/products/models" 9 | "go-license-management/internal/utils" 10 | "go.opentelemetry.io/otel/trace" 11 | "time" 12 | ) 13 | 14 | type ProductRegistrationRequest struct { 15 | product_attribute.ProductAttribute 16 | } 17 | 18 | func (req *ProductRegistrationRequest) Validate() error { 19 | if req.Name == nil { 20 | return cerrors.ErrProductNameIsEmpty 21 | } 22 | 23 | if req.Code == nil { 24 | return cerrors.ErrProductCodeIsEmpty 25 | } 26 | 27 | if req.DistributionStrategy == nil { 28 | req.DistributionStrategy = utils.RefPointer(constants.ProductDistributionStrategyLicensed) 29 | } else { 30 | if _, ok := constants.ValidProductDistributionStrategyMapper[utils.DerefPointer(req.DistributionStrategy)]; !ok { 31 | return cerrors.ErrProductDistributionStrategyIsInvalid 32 | } 33 | } 34 | return nil 35 | } 36 | 37 | func (req *ProductRegistrationRequest) ToProductRegistrationInput(ctx context.Context, tracer trace.Tracer, productURI product_attribute.ProductCommonURI) *models.ProductRegistrationInput { 38 | return &models.ProductRegistrationInput{ 39 | TracerCtx: ctx, 40 | Tracer: tracer, 41 | ProductCommonURI: productURI, 42 | ProductAttribute: req.ProductAttribute, 43 | } 44 | } 45 | 46 | type ProductUpdateRequest struct { 47 | product_attribute.ProductAttribute 48 | } 49 | 50 | func (req *ProductUpdateRequest) Validate() error { 51 | if req.DistributionStrategy != nil { 52 | if _, ok := constants.ValidProductDistributionStrategyMapper[utils.DerefPointer(req.DistributionStrategy)]; !ok { 53 | return cerrors.ErrProductDistributionStrategyIsInvalid 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | func (req *ProductUpdateRequest) ToProductUpdateInput(ctx context.Context, tracer trace.Tracer, productURI product_attribute.ProductCommonURI) *models.ProductUpdateInput { 60 | return &models.ProductUpdateInput{ 61 | TracerCtx: ctx, 62 | Tracer: tracer, 63 | ProductCommonURI: productURI, 64 | ProductAttribute: req.ProductAttribute, 65 | } 66 | } 67 | 68 | type ProductListRequest struct { 69 | constants.QueryCommonParam 70 | } 71 | 72 | func (req *ProductListRequest) Validate() error { 73 | req.QueryCommonParam.Validate() 74 | return nil 75 | } 76 | 77 | func (req *ProductListRequest) ToProductListInput(ctx context.Context, tracer trace.Tracer, productURI product_attribute.ProductCommonURI) *models.ProductListInput { 78 | return &models.ProductListInput{ 79 | TracerCtx: ctx, 80 | Tracer: tracer, 81 | ProductCommonURI: productURI, 82 | QueryCommonParam: req.QueryCommonParam, 83 | } 84 | } 85 | 86 | type ProductRetrievalRequest struct { 87 | product_attribute.ProductCommonURI 88 | } 89 | 90 | func (req *ProductRetrievalRequest) Validate() error { 91 | if req.ProductID == nil { 92 | return cerrors.ErrProductIDIsEmpty 93 | } 94 | return req.ProductCommonURI.Validate() 95 | } 96 | 97 | func (req *ProductRetrievalRequest) ToProductRetrievalInput(ctx context.Context, tracer trace.Tracer) *models.ProductRetrievalInput { 98 | return &models.ProductRetrievalInput{ 99 | TracerCtx: ctx, 100 | Tracer: tracer, 101 | ProductCommonURI: req.ProductCommonURI, 102 | } 103 | } 104 | 105 | type ProductDeletionRequest struct { 106 | product_attribute.ProductCommonURI 107 | } 108 | 109 | func (req *ProductDeletionRequest) Validate() error { 110 | if req.ProductID == nil { 111 | return cerrors.ErrProductIDIsEmpty 112 | } 113 | return req.ProductCommonURI.Validate() 114 | } 115 | 116 | func (req *ProductDeletionRequest) ToProductDeletionInput(ctx context.Context, tracer trace.Tracer) *models.ProductDeletionInput { 117 | return &models.ProductDeletionInput{ 118 | TracerCtx: ctx, 119 | Tracer: tracer, 120 | ProductCommonURI: req.ProductCommonURI, 121 | } 122 | } 123 | 124 | type ProductTokenRequest struct { 125 | Name *string `json:"name" validate:"optional" example:"test"` 126 | Expiry *string `json:"expiry" validate:"optional" example:"test"` 127 | Permissions []string `json:"permissions" validate:"required" example:"test"` 128 | } 129 | 130 | func (req *ProductTokenRequest) Validate() error { 131 | 132 | if req.Expiry != nil { 133 | _, err := time.Parse(constants.DateFormatISO8601Hyphen, utils.DerefPointer(req.Expiry)) 134 | if err != nil { 135 | return cerrors.ErrProductTokenExpirationFormatIsInvalid 136 | } 137 | } 138 | if req.Permissions == nil { 139 | req.Permissions = []string{"*"} 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (req *ProductTokenRequest) ToProductTokenInput(ctx context.Context, tracer trace.Tracer, productURI product_attribute.ProductCommonURI) *models.ProductTokensInput { 146 | return &models.ProductTokensInput{ 147 | TracerCtx: ctx, 148 | Tracer: tracer, 149 | ProductCommonURI: productURI, 150 | Name: req.Name, 151 | Expiry: req.Expiry, 152 | Permissions: req.Permissions, 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /server/api/v1/authentications/authentication.go: -------------------------------------------------------------------------------- 1 | package authentications 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "go-license-management/internal/cerrors" 7 | "go-license-management/internal/config" 8 | "go-license-management/internal/constants" 9 | "go-license-management/internal/infrastructure/logging" 10 | "go-license-management/internal/infrastructure/models/authentication_attribute" 11 | "go-license-management/internal/infrastructure/tracer" 12 | "go-license-management/internal/response" 13 | "go-license-management/internal/services/v1/authentications/service" 14 | "go-license-management/internal/utils" 15 | "go.opentelemetry.io/otel/attribute" 16 | "go.opentelemetry.io/otel/trace" 17 | "go.uber.org/zap" 18 | "net/http" 19 | ) 20 | 21 | type AuthenticationRouter struct { 22 | svc *service.AuthenticationService 23 | logger *logging.Logger 24 | tracer trace.Tracer 25 | } 26 | 27 | func NewAuthenticationRouter(svc *service.AuthenticationService) *AuthenticationRouter { 28 | tr := tracer.GetInstance().Tracer("auth_group") 29 | logger := logging.NewECSLogger() 30 | return &AuthenticationRouter{ 31 | svc: svc, 32 | logger: logger, 33 | tracer: tr, 34 | } 35 | } 36 | 37 | func (r *AuthenticationRouter) Routes(engine *gin.RouterGroup, path string) { 38 | routes := engine.Group(path) 39 | { 40 | routes = routes.Group("/auth") 41 | routes.POST("/login", r.login) 42 | } 43 | } 44 | 45 | // login validates existing account resource. 46 | // 47 | // @Summary API to validate existing account and return a corresponding jwt token 48 | // @Description Validating account and generate a JWT token if valid, without tenant_name path parameter, one must provide the superadmin credentials 49 | // @Tags authentication 50 | // @Accept mpfd 51 | // @Produce json 52 | // @Param username formData string true "username" 53 | // @Param password formData string true "password" 54 | // @Param tenant_name path string true "tenant_name" 55 | // @Success 200 {object} response.Response 56 | // @Failure 400 {object} response.Response 57 | // @Failure 500 {object} response.Response 58 | // @Router /tenants/{tenant_name}/auth/login [post] 59 | // @Router /auth/login [post] 60 | func (r *AuthenticationRouter) login(ctx *gin.Context) { 61 | rootCtx, span := r.tracer.Start(ctx, ctx.Request.URL.Path, trace.WithAttributes(attribute.KeyValue{ 62 | Key: constants.RequestIDField, 63 | Value: attribute.StringValue(ctx.GetString(constants.RequestIDField)), 64 | })) 65 | defer span.End() 66 | 67 | resp := response.NewResponse(ctx) 68 | r.logger.WithCustomFields(zap.String(constants.RequestIDField, ctx.GetString(constants.RequestIDField))).Info("received new account login request") 69 | 70 | // serializer 71 | var uriReq authentication_attribute.AuthenticationCommonURI 72 | _, cSpan := r.tracer.Start(rootCtx, "serializer") 73 | err := ctx.ShouldBindUri(&uriReq) 74 | if err != nil { 75 | cSpan.End() 76 | r.logger.GetLogger().Error(err.Error()) 77 | resp.ToResponse(cerrors.ErrCodeMapper[cerrors.ErrGenericBadRequest], cerrors.ErrMessageMapper[cerrors.ErrGenericBadRequest], nil, nil, nil) 78 | ctx.JSON(http.StatusBadRequest, resp) 79 | return 80 | } 81 | 82 | var bodyReq AuthenticationLoginRequest 83 | err = ctx.ShouldBind(&bodyReq) 84 | if err != nil { 85 | cSpan.End() 86 | r.logger.GetLogger().Error(err.Error()) 87 | resp.ToResponse(cerrors.ErrCodeMapper[cerrors.ErrGenericBadRequest], cerrors.ErrMessageMapper[cerrors.ErrGenericBadRequest], nil, nil, nil) 88 | ctx.JSON(http.StatusBadRequest, resp) 89 | return 90 | } 91 | cSpan.End() 92 | 93 | // validation 94 | _, cSpan = r.tracer.Start(rootCtx, "validation") 95 | err = bodyReq.Validate() 96 | if err != nil { 97 | cSpan.End() 98 | r.logger.GetLogger().Error(err.Error()) 99 | resp.ToResponse(cerrors.ErrCodeMapper[err], cerrors.ErrMessageMapper[err], nil, nil, nil) 100 | ctx.JSON(http.StatusBadRequest, resp) 101 | return 102 | } 103 | 104 | // Super admin user can login with all paths 105 | if utils.DerefPointer(bodyReq.Username) != config.SuperAdminUsername { 106 | err = uriReq.Validate() 107 | if err != nil { 108 | cSpan.End() 109 | r.logger.GetLogger().Error(err.Error()) 110 | resp.ToResponse(cerrors.ErrCodeMapper[err], cerrors.ErrMessageMapper[err], nil, nil, nil) 111 | ctx.JSON(http.StatusBadRequest, resp) 112 | return 113 | } 114 | } 115 | cSpan.End() 116 | 117 | // handler 118 | _, cSpan = r.tracer.Start(rootCtx, "handler") 119 | result, err := r.svc.Login(ctx, bodyReq.ToAuthenticationLoginInput(rootCtx, r.tracer, uriReq)) 120 | if err != nil { 121 | cSpan.End() 122 | r.logger.GetLogger().Error(err.Error()) 123 | resp.ToResponse(result.Code, result.Message, result.Data, nil, nil) 124 | switch { 125 | case errors.Is(err, cerrors.ErrGenericUnauthorized), 126 | errors.Is(err, cerrors.ErrAccountIsBanned), 127 | errors.Is(err, cerrors.ErrAccountIsInactive): 128 | ctx.JSON(http.StatusUnauthorized, resp) 129 | default: 130 | ctx.JSON(http.StatusInternalServerError, resp) 131 | } 132 | return 133 | } 134 | cSpan.End() 135 | 136 | resp.ToResponse(result.Code, result.Message, result.Data, nil, nil) 137 | ctx.JSON(http.StatusOK, resp) 138 | return 139 | } 140 | -------------------------------------------------------------------------------- /internal/repositories/v1/licenses/repository.go: -------------------------------------------------------------------------------- 1 | package licenses 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "github.com/uptrace/bun" 7 | "go-license-management/internal/cerrors" 8 | "go-license-management/internal/constants" 9 | "go-license-management/internal/infrastructure/database/entities" 10 | "go-license-management/internal/utils" 11 | "go-license-management/server/api" 12 | "time" 13 | ) 14 | 15 | type LicenseRepository struct { 16 | database *bun.DB 17 | } 18 | 19 | func NewLicenseRepository(ds *api.DataSource) *LicenseRepository { 20 | return &LicenseRepository{ 21 | database: ds.GetDatabase(), 22 | } 23 | } 24 | 25 | func (repo *LicenseRepository) SelectTenantByName(ctx context.Context, tenantName string) (*entities.Tenant, error) { 26 | if repo.database == nil { 27 | return nil, cerrors.ErrInvalidDatabaseClient 28 | } 29 | 30 | tenant := &entities.Tenant{Name: tenantName} 31 | 32 | err := repo.database.NewSelect().Model(tenant).WherePK().Scan(ctx) 33 | if err != nil { 34 | return tenant, err 35 | } 36 | 37 | return tenant, nil 38 | } 39 | 40 | func (repo *LicenseRepository) SelectProductByPK(ctx context.Context, productID uuid.UUID) (*entities.Product, error) { 41 | if repo.database == nil { 42 | return nil, cerrors.ErrInvalidDatabaseClient 43 | } 44 | 45 | product := &entities.Product{ID: productID} 46 | 47 | err := repo.database.NewSelect().Model(product).WherePK().Scan(ctx) 48 | if err != nil { 49 | return product, err 50 | } 51 | 52 | return product, nil 53 | } 54 | 55 | func (repo *LicenseRepository) SelectPolicyByPK(ctx context.Context, policyID uuid.UUID) (*entities.Policy, error) { 56 | if repo.database == nil { 57 | return nil, cerrors.ErrInvalidDatabaseClient 58 | } 59 | 60 | policy := &entities.Policy{ID: policyID} 61 | 62 | err := repo.database.NewSelect().Model(policy).WherePK().Scan(ctx) 63 | if err != nil { 64 | return policy, err 65 | } 66 | 67 | return policy, nil 68 | } 69 | 70 | func (repo *LicenseRepository) InsertNewLicense(ctx context.Context, license *entities.License) error { 71 | if repo.database == nil { 72 | return cerrors.ErrInvalidDatabaseClient 73 | } 74 | 75 | _, err := repo.database.NewInsert().Model(license).Exec(ctx) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (repo *LicenseRepository) SelectLicenseByPK(ctx context.Context, licenseID uuid.UUID) (*entities.License, error) { 84 | if repo.database == nil { 85 | return nil, cerrors.ErrInvalidDatabaseClient 86 | } 87 | 88 | license := &entities.License{ID: licenseID} 89 | 90 | err := repo.database.NewSelect().Model(license).Relation("Policy").Relation("Product").WherePK().Scan(ctx) 91 | if err != nil { 92 | return license, err 93 | } 94 | 95 | return license, nil 96 | } 97 | 98 | func (repo *LicenseRepository) SelectLicenses(ctx context.Context, tenantName string, queryParam constants.QueryCommonParam) ([]entities.License, int, error) { 99 | var total = 0 100 | 101 | if repo.database == nil { 102 | return nil, total, cerrors.ErrInvalidDatabaseClient 103 | } 104 | 105 | licenses := make([]entities.License, 0) 106 | total, err := repo.database.NewSelect().Model(new(entities.License)). 107 | Where("tenant_name = ?", tenantName). 108 | Order("created_at DESC"). 109 | Limit(utils.DerefPointer(queryParam.Limit)). 110 | Offset(utils.DerefPointer(queryParam.Offset)). 111 | ScanAndCount(ctx, &licenses) 112 | if err != nil { 113 | return licenses, total, err 114 | } 115 | return licenses, total, nil 116 | } 117 | 118 | func (repo *LicenseRepository) SelectLicenseByLicenseKey(ctx context.Context, licenseKey string) (*entities.License, error) { 119 | if repo.database == nil { 120 | return nil, cerrors.ErrInvalidDatabaseClient 121 | } 122 | 123 | license := &entities.License{Key: licenseKey} 124 | 125 | err := repo.database.NewSelect().Model(license).Relation("Policy").Relation("Product").Where("key = ?", licenseKey).Scan(ctx) 126 | if err != nil { 127 | return license, err 128 | } 129 | 130 | return license, nil 131 | } 132 | 133 | func (repo *LicenseRepository) DeleteLicenseByPK(ctx context.Context, licenseID uuid.UUID) error { 134 | if repo.database == nil { 135 | return cerrors.ErrInvalidDatabaseClient 136 | } 137 | 138 | license := &entities.License{ID: licenseID} 139 | 140 | _, err := repo.database.NewDelete().Model(license).WherePK().Exec(ctx) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | return nil 146 | } 147 | 148 | func (repo *LicenseRepository) UpdateLicenseByPK(ctx context.Context, license *entities.License) (*entities.License, error) { 149 | if repo.database == nil { 150 | return license, cerrors.ErrInvalidDatabaseClient 151 | } 152 | 153 | license.UpdatedAt = time.Now() 154 | _, err := repo.database.NewUpdate().Model(license).WherePK().Exec(ctx) 155 | if err != nil { 156 | return license, err 157 | } 158 | 159 | return license, nil 160 | } 161 | 162 | func (repo *LicenseRepository) CheckPolicyExist(ctx context.Context, policyID uuid.UUID) (bool, error) { 163 | var exist bool 164 | 165 | if repo.database == nil { 166 | return exist, cerrors.ErrInvalidDatabaseClient 167 | } 168 | 169 | policy := &entities.Policy{ID: policyID} 170 | exist, err := repo.database.NewSelect().Model(policy).WherePK().Exists(ctx) 171 | if err != nil { 172 | return exist, err 173 | } 174 | 175 | return exist, nil 176 | } 177 | 178 | func (repo *LicenseRepository) CheckProductExist(ctx context.Context, productID uuid.UUID) (bool, error) { 179 | var exist bool 180 | 181 | if repo.database == nil { 182 | return exist, cerrors.ErrInvalidDatabaseClient 183 | } 184 | 185 | product := &entities.Product{ID: productID} 186 | exist, err := repo.database.NewSelect().Model(product).WherePK().Exists(ctx) 187 | if err != nil { 188 | return exist, err 189 | } 190 | 191 | return exist, nil 192 | } 193 | -------------------------------------------------------------------------------- /internal/services/v1/licenses/models/license_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "go-license-management/internal/constants" 6 | "go-license-management/internal/infrastructure/models/license_attribute" 7 | "go.opentelemetry.io/otel/trace" 8 | "time" 9 | ) 10 | 11 | type LicenseRegistrationInput struct { 12 | TracerCtx context.Context 13 | Tracer trace.Tracer 14 | license_attribute.LicenseCommonURI 15 | PolicyID *string `json:"policy_id" validate:"required" example:"test"` 16 | ProductID *string `json:"product_id" validate:"required" example:"test"` 17 | Name *string `json:"name" validate:"required" example:"test"` 18 | MaxMachines *int `json:"max_machines" validate:"optional" example:"test"` 19 | MaxUsers *int `json:"max_users" validate:"optional" example:"test"` 20 | MaxUses *int `json:"max_uses" validate:"optional" example:"test"` 21 | Expiry *string `json:"expiry" validate:"optional" example:"test"` 22 | Metadata map[string]interface{} `json:"metadata" validate:"optional" example:"test"` 23 | } 24 | 25 | type LicenseUpdateInput struct { 26 | TracerCtx context.Context 27 | Tracer trace.Tracer 28 | license_attribute.LicenseCommonURI 29 | PolicyID *string `json:"policy_id" validate:"required" example:"test"` 30 | ProductID *string `json:"product_id" validate:"required" example:"test"` 31 | Name *string `json:"name" validate:"required" example:"test"` 32 | MaxMachines *int `json:"max_machines" validate:"optional" example:"test"` 33 | MaxUsers *int `json:"max_users" validate:"optional" example:"test"` 34 | MaxUses *int `json:"max_uses" validate:"optional" example:"test"` 35 | Expiry *string `json:"expiry" validate:"optional" example:"test"` 36 | Metadata map[string]interface{} `json:"metadata" validate:"optional" example:"test"` 37 | } 38 | 39 | type LicenseRetrievalInput struct { 40 | TracerCtx context.Context 41 | Tracer trace.Tracer 42 | license_attribute.LicenseCommonURI 43 | } 44 | 45 | type LicenseInfoOutput struct { 46 | LicenseID string `json:"license_id"` 47 | ProductID string `json:"product_id"` 48 | PolicyID string `json:"policy_id"` 49 | Name string `json:"name"` 50 | LicenseKey string `json:"license_key"` 51 | MD5Checksum string `json:"md5_checksum"` 52 | Sha1Checksum string `json:"sha1_checksum"` 53 | Sha256Checksum string `json:"sha256_checksum"` 54 | Status string `json:"status"` 55 | Metadata map[string]interface{} `json:"metadata"` 56 | Expiry time.Time `json:"expiry"` 57 | CreatedAt time.Time `json:"created_at"` 58 | UpdatedAt time.Time `json:"updated_at"` 59 | LicensePolicy LicensePolicyOutput `json:"license_policy"` 60 | LicenseProduct LicenseProductOutput `json:"license_product"` 61 | } 62 | 63 | type LicenseProductOutput struct { 64 | Name string `json:"name,type:varchar(256)"` 65 | DistributionStrategy string `json:"distribution_strategy,type:varchar(128)"` 66 | Code string `json:"code,type:varchar(128),unique"` 67 | URL string `json:"url,type:varchar(1024)"` 68 | Platforms []string `json:"platform,type:jsonb"` 69 | Metadata map[string]interface{} `json:"metadata,type:jsonb"` 70 | CreatedAt time.Time `json:"created_at,nullzero,notnull,default:current_timestamp"` 71 | UpdatedAt time.Time `json:"updated_at,nullzero,notnull,default:current_timestamp"` 72 | } 73 | 74 | type LicensePolicyOutput struct { 75 | PolicyPublicKey string `json:"policy_public_key"` 76 | PolicyScheme string `json:"policy_scheme"` 77 | ExpirationStrategy string `json:"expiration_strategy"` 78 | CheckInInterval string `json:"check_in_interval"` 79 | OverageStrategy string `json:"overage_strategy"` 80 | HeartbeatBasis string `json:"heartbeat_basis"` 81 | RenewalBasis string `json:"renewal_basis"` 82 | RequireCheckIn bool `json:"require_check_in"` 83 | Concurrent bool `json:"concurrent"` 84 | RequireHeartbeat bool `json:"require_heartbeat"` 85 | Strict bool `json:"strict"` 86 | Floating bool `json:"floating"` 87 | UsePool bool `json:"use_pool"` 88 | RateLimited bool `json:"rate_limited"` 89 | Encrypted bool `json:"encrypted"` 90 | Protected bool `json:"protected"` 91 | Duration int64 `json:"duration"` 92 | MaxMachines int `json:"max_machines"` 93 | MaxUses int `json:"max_uses"` 94 | MaxUsers int `json:"max_users"` 95 | HeartbeatDuration int `json:"heartbeat_duration"` 96 | } 97 | 98 | type LicenseListInput struct { 99 | TracerCtx context.Context 100 | Tracer trace.Tracer 101 | license_attribute.LicenseCommonURI 102 | constants.QueryCommonParam 103 | } 104 | 105 | type LicenseDeletionInput struct { 106 | TracerCtx context.Context 107 | Tracer trace.Tracer 108 | license_attribute.LicenseCommonURI 109 | } 110 | 111 | type LicenseActionInput struct { 112 | TracerCtx context.Context 113 | Tracer trace.Tracer 114 | license_attribute.LicenseCommonURI 115 | LicenseKey *string `json:"license_key"` 116 | Nonce *int `json:"nonce"` 117 | Increment *int `json:"increment"` 118 | Decrement *int `json:"decrement"` 119 | } 120 | 121 | type LicenseValidationOutput struct { 122 | Valid bool `json:"valid"` 123 | Code string `json:"code"` 124 | } 125 | 126 | type LicenseActionCheckoutOutput struct { 127 | Certificate string `json:"certificate"` 128 | TTL int `json:"ttl"` 129 | ExpiryAt time.Time `json:"expiry_at"` 130 | IssuedAt time.Time `json:"issued_at"` 131 | } 132 | -------------------------------------------------------------------------------- /internal/utils/ed25519_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/x509" 6 | "encoding/hex" 7 | "fmt" 8 | "github.com/golang-jwt/jwt/v5" 9 | "github.com/stretchr/testify/assert" 10 | "testing" 11 | ) 12 | 13 | func TestNewEd25519KeyPair(t *testing.T) { 14 | signingKey, verifyKey, err := NewEd25519KeyPair() 15 | assert.NoError(t, err) 16 | 17 | fmt.Println(signingKey) 18 | fmt.Println(verifyKey) 19 | } 20 | 21 | func TestLicenseKeyWithEd25519(t *testing.T) { 22 | licenseKey, err := NewLicenseKeyWithEd25519("302e020100300506032b6570042204201411064cece60c82fe80dd7ca82c6239fd7f2094fcfb5f27405e23b38c7cae9a", "sart") 23 | assert.NoError(t, err) 24 | 25 | fmt.Println(licenseKey) 26 | } 27 | 28 | func TestVerifyLicenseKeyWithEd25519(t *testing.T) { 29 | signingKey, verifyKey, err := NewEd25519KeyPair() 30 | assert.NoError(t, err) 31 | 32 | fmt.Println(signingKey) 33 | fmt.Println(verifyKey) 34 | 35 | licenseKey, err := NewLicenseKeyWithEd25519(signingKey, "sart") 36 | assert.NoError(t, err) 37 | 38 | fmt.Println(licenseKey) 39 | 40 | valid, data, err := VerifyLicenseKeyWithEd25519(verifyKey, licenseKey) 41 | assert.NoError(t, err) 42 | 43 | fmt.Println(valid) 44 | fmt.Println(data, string(data)) 45 | } 46 | 47 | func TestDecodeJWT(t *testing.T) { 48 | token := "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhZG1pbiIsImV4cCI6MTczNTYyMzk5MSwiaWF0IjoxNzM1NjIwMzkxLCJpc3MiOiJnby1saWNlbnNlLW1hbmFnZW1lbnQiLCJuYmYiOjE3MzU2MjAzOTEsInBlcm1pc3Npb25zIjpbImxpY2Vuc2UtZW50aXRsZW1lbnRzLmRldGFjaCIsImxpY2Vuc2UtdXNlcnMuYXR0YWNoIiwicG9saWN5X2VudGl0bGVtZW50cy5hdHRhY2giLCJwb2xpY3lfZW50aXRsZW1lbnRzLmRldGFjaCIsImxpY2Vuc2UtZW50aXRsZW1lbnRzLmF0dGFjaCIsIm1hY2hpbmUtaGVhcnRiZWF0LnJlc2V0IiwidGVuYW50LnJlYWQiLCJtYWNoaW5lLmNoZWNrLW91dCIsIm1hY2hpbmUuZGVsZXRlIiwibWFjaGluZS5yZWFkIiwiYWRtaW4uZGVsZXRlIiwidXNlcl9wYXNzd29yZC5yZXNldCIsImVudGl0bGVtZW50LnJlYWQiLCJsaWNlbnNlLmNoZWNrLW91dCIsInVzZXIuZGVsZXRlIiwicG9saWN5LmRlbGV0ZSIsImxpY2Vuc2UudXBkYXRlIiwibWFjaGluZS51cGRhdGUiLCJwcm9kdWN0X3Rva2Vucy5nZW5lcmF0ZSIsInBvbGljeS5jcmVhdGUiLCJsaWNlbnNlLXVzYWdlLnJlc2V0IiwibGljZW5zZS1wb2xpY3kudXBkYXRlIiwibWFjaGluZS5jcmVhdGUiLCJ0ZW5hbnQudXBkYXRlIiwiYWRtaW4uY3JlYXRlIiwidXNlci5iYW4iLCJ1c2VyX3Bhc3N3b3JkLnVwZGF0ZSIsImxpY2Vuc2UucmV2b2tlIiwibGljZW5zZS11c2FnZS5kZWNyZW1lbnQiLCJsaWNlbnNlLXRva2Vucy5nZW5lcmF0ZSIsInByb2R1Y3QuY3JlYXRlIiwicHJvZHVjdC5kZWxldGUiLCJwcm9kdWN0LnVwZGF0ZSIsImxpY2Vuc2UuY3JlYXRlIiwibGljZW5zZS11c2FnZS5pbmNyZW1lbnQiLCJtYWNoaW5lLWhlYXJ0YmVhdC5waW5nIiwicG9saWN5LnJlYWQiLCJsaWNlbnNlLnJlYWQiLCJ1c2VyLnJlYWQiLCJ1c2VyLnVuYmFuIiwidXNlci51cGRhdGUiLCJlbnRpdGxlbWVudC5jcmVhdGUiLCJsaWNlbnNlLmNoZWNrLWluIiwibGljZW5zZS11c2Vycy5kZXRhY2giLCJhZG1pbi51cGRhdGUiLCJwb2xpY3kudXBkYXRlIiwibGljZW5zZS5yZW5ldyIsInRlbmFudC5jcmVhdGUiLCJwcm9kdWN0LnJlYWQiLCJsaWNlbnNlLnZhbGlkYXRlIiwidXNlci5jcmVhdGUiLCJlbnRpdGxlbWVudC51cGRhdGUiLCJsaWNlbnNlLnJlaW5zdGF0ZSIsImxpY2Vuc2Uuc3VzcGVuZCIsImFkbWluLnJlYWQiLCJlbnRpdGxlbWVudC5kZWxldGUiLCJ0ZW5hbnQuZGVsZXRlIiwibGljZW5zZS5kZWxldGUiXSwic3ViIjoidXNlcjEiLCJ0ZW5hbnQiOiJ0ZXN0In0.vVSbLQgPTyezexIS5mfE0FRqcGCNOxY6ZxoZJW60tXAGNWdr2gjjjD0B9W-zfoCWv_N5J6VhV-AgfCG9rs4xBQ" 49 | 50 | publicKey, err := hex.DecodeString("302a300506032b6570032100462c6f6e52b3b35f8546a8447865c09b6ace033101c43bddd0fcc181a0fd776b") 51 | assert.NoError(t, err) 52 | 53 | decodedPublicKey, err := x509.ParsePKIXPublicKey(publicKey) 54 | assert.NoError(t, err) 55 | 56 | publicKey, ok := decodedPublicKey.(ed25519.PublicKey) 57 | assert.True(t, ok) 58 | 59 | parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { 60 | if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { 61 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 62 | } 63 | return decodedPublicKey, nil 64 | }) 65 | assert.NoError(t, err) 66 | 67 | fmt.Println(parsedToken.Claims.(jwt.MapClaims)) 68 | 69 | } 70 | 71 | func TestDecodeSuperJWT(t *testing.T) { 72 | token := "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJzdXBlcmFkbWluIiwiZXhwIjoxNzM1ODc0NTU0LCJpYXQiOjE3MzU4NzA5NTQsImlzcyI6ImdvLWxpY2Vuc2UtbWFuYWdlbWVudCIsIm5iZiI6MTczNTg3MDk1NCwicGVybWlzc2lvbnMiOlsidGVuYW50LnVwZGF0ZSIsInVzZXIudW5iYW4iLCJwcm9kdWN0LnVwZGF0ZSIsImxpY2Vuc2UucmVpbnN0YXRlIiwidGVuYW50LmNyZWF0ZSIsImFkbWluLmRlbGV0ZSIsImVudGl0bGVtZW50LmNyZWF0ZSIsImxpY2Vuc2UtZW50aXRsZW1lbnRzLmF0dGFjaCIsInVzZXIuY3JlYXRlIiwibWFjaGluZS5jaGVjay1vdXQiLCJtYWNoaW5lLWhlYXJ0YmVhdC5yZXNldCIsInVzZXJfcGFzc3dvcmQudXBkYXRlIiwibGljZW5zZS5kZWxldGUiLCJsaWNlbnNlLXRva2Vucy5nZW5lcmF0ZSIsImxpY2Vuc2UtdXNhZ2UucmVzZXQiLCJwb2xpY3kuY3JlYXRlIiwibGljZW5zZS5yZXZva2UiLCJ0ZW5hbnQucmVhZCIsImxpY2Vuc2UucmVhZCIsImxpY2Vuc2UudmFsaWRhdGUiLCJhZG1pbi5jcmVhdGUiLCJwb2xpY3lfZW50aXRsZW1lbnRzLmRldGFjaCIsImxpY2Vuc2UucmVuZXciLCJtYWNoaW5lLnJlYWQiLCJwcm9kdWN0LnJlYWQiLCJsaWNlbnNlLWVudGl0bGVtZW50cy5kZXRhY2giLCJ1c2VyLnVwZGF0ZSIsInByb2R1Y3QuZGVsZXRlIiwibGljZW5zZS1wb2xpY3kudXBkYXRlIiwiYWRtaW4ucmVhZCIsImVudGl0bGVtZW50LnVwZGF0ZSIsInByb2R1Y3QuY3JlYXRlIiwidXNlcl9wYXNzd29yZC5yZXNldCIsInBvbGljeV9lbnRpdGxlbWVudHMuYXR0YWNoIiwibGljZW5zZS11c2FnZS5pbmNyZW1lbnQiLCJsaWNlbnNlLXVzZXJzLmF0dGFjaCIsImxpY2Vuc2UuY3JlYXRlIiwibGljZW5zZS5zdXNwZW5kIiwibGljZW5zZS51cGRhdGUiLCJ0ZW5hbnQuZGVsZXRlIiwidXNlci5kZWxldGUiLCJ1c2VyLnJlYWQiLCJwcm9kdWN0X3Rva2Vucy5nZW5lcmF0ZSIsImxpY2Vuc2UuY2hlY2staW4iLCJhZG1pbi51cGRhdGUiLCJwb2xpY3kucmVhZCIsImxpY2Vuc2UtdXNhZ2UuZGVjcmVtZW50IiwibWFjaGluZS5jcmVhdGUiLCJ1c2VyLmJhbiIsImxpY2Vuc2UtdXNlcnMuZGV0YWNoIiwibWFjaGluZS5kZWxldGUiLCJlbnRpdGxlbWVudC5yZWFkIiwicG9saWN5LnVwZGF0ZSIsIm1hY2hpbmUudXBkYXRlIiwiZW50aXRsZW1lbnQuZGVsZXRlIiwicG9saWN5LmRlbGV0ZSIsImxpY2Vuc2UuY2hlY2stb3V0IiwibWFjaGluZS1oZWFydGJlYXQucGluZyJdLCJzdWIiOiJzdXBlcmFkbWluIiwidGVuYW50IjoiKiJ9.IGFoGW_VTo2mfoHsLmeneT0j7X0wMiCv1VDL9AWpzH8d7MRuw8pBsMlzp5XfndkZf3xkkF2M-lu0wXHH94OtCw" 73 | 74 | publicKey, err := hex.DecodeString("302a300506032b6570032100d8f20a6c88fb46c337e51b4cd43f3f83e04924f811302e815967f67b849cea87") 75 | assert.NoError(t, err) 76 | 77 | decodedPublicKey, err := x509.ParsePKIXPublicKey(publicKey) 78 | assert.NoError(t, err) 79 | 80 | publicKey, ok := decodedPublicKey.(ed25519.PublicKey) 81 | assert.True(t, ok) 82 | 83 | parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { 84 | if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { 85 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 86 | } 87 | return decodedPublicKey, nil 88 | }) 89 | assert.NoError(t, err) 90 | 91 | fmt.Println(parsedToken.Claims.(jwt.MapClaims)) 92 | 93 | } 94 | --------------------------------------------------------------------------------