Basis perm nodes feature

This commit is contained in:
LittleSheep 2024-05-17 17:13:11 +08:00
parent 0e89e325d4
commit 7d3b804516
13 changed files with 124 additions and 32 deletions

View File

@ -4,9 +4,9 @@
<option name="autoReloadType" value="ALL" /> <option name="autoReloadType" value="ALL" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":bug: Fix key exchange cause echo"> <list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Basis perm nodes feature">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/services/notifications.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/notifications.go" afterDir="false" /> <change beforePath="$PROJECT_DIR$/pkg/server/ws.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ws.go" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -139,8 +139,6 @@
</option> </option>
</component> </component>
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value=":sparkles: Sign up &amp; Sign in" />
<MESSAGE value=":sparkles: An entire complete sign in user flow" />
<MESSAGE value=":sparkles: User center page" /> <MESSAGE value=":sparkles: User center page" />
<MESSAGE value=":sparkles: Personalize" /> <MESSAGE value=":sparkles: Personalize" />
<MESSAGE value=":sparkles: OAuth" /> <MESSAGE value=":sparkles: OAuth" />
@ -164,7 +162,9 @@
<MESSAGE value=":sparkles: E2EE Key Exchange" /> <MESSAGE value=":sparkles: E2EE Key Exchange" />
<MESSAGE value=":bug: Bug fixes on E2EE" /> <MESSAGE value=":bug: Bug fixes on E2EE" />
<MESSAGE value=":bug: Fix key exchange cause echo" /> <MESSAGE value=":bug: Fix key exchange cause echo" />
<option name="LAST_COMMIT_MESSAGE" value=":bug: Fix key exchange cause echo" /> <MESSAGE value=":bug: Fix notification push issue" />
<MESSAGE value=":sparkles: Basis perm nodes feature" />
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: Basis perm nodes feature" />
</component> </component>
<component name="VgoProject"> <component name="VgoProject">
<settings-migrated>true</settings-migrated> <settings-migrated>true</settings-migrated>

View File

@ -3,6 +3,7 @@ package grpc
import ( import (
"context" "context"
"fmt" "fmt"
jsoniter "github.com/json-iterator/go"
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto" "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
"git.solsynth.dev/hydrogen/passport/pkg/services" "git.solsynth.dev/hydrogen/passport/pkg/services"
@ -10,16 +11,18 @@ import (
) )
func (v *Server) Authenticate(_ context.Context, in *proto.AuthRequest) (*proto.AuthReply, error) { func (v *Server) Authenticate(_ context.Context, in *proto.AuthRequest) (*proto.AuthReply, error) {
user, atk, rtk, err := services.Authenticate(in.GetAccessToken(), in.GetRefreshToken(), 0) user, perms, atk, rtk, err := services.Authenticate(in.GetAccessToken(), in.GetRefreshToken(), 0)
if err != nil { if err != nil {
return &proto.AuthReply{ return &proto.AuthReply{
IsValid: false, IsValid: false,
}, nil }, nil
} else { } else {
rawPerms, _ := jsoniter.Marshal(perms)
return &proto.AuthReply{ return &proto.AuthReply{
IsValid: true, IsValid: true,
AccessToken: &atk, AccessToken: &atk,
RefreshToken: &rtk, RefreshToken: &rtk,
Permissions: rawPerms,
Userinfo: &proto.Userinfo{ Userinfo: &proto.Userinfo{
Id: uint64(user.ID), Id: uint64(user.ID),
Name: user.Name, Name: user.Name,

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.33.0 // protoc-gen-go v1.33.0
// protoc v4.25.3 // protoc v5.26.1
// source: auth.proto // source: auth.proto
package proto package proto
@ -179,6 +179,7 @@ type AuthReply struct {
AccessToken *string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3,oneof" json:"access_token,omitempty"` AccessToken *string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3,oneof" json:"access_token,omitempty"`
RefreshToken *string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3,oneof" json:"refresh_token,omitempty"` RefreshToken *string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3,oneof" json:"refresh_token,omitempty"`
Userinfo *Userinfo `protobuf:"bytes,4,opt,name=userinfo,proto3,oneof" json:"userinfo,omitempty"` Userinfo *Userinfo `protobuf:"bytes,4,opt,name=userinfo,proto3,oneof" json:"userinfo,omitempty"`
Permissions []byte `protobuf:"bytes,5,opt,name=permissions,proto3,oneof" json:"permissions,omitempty"`
} }
func (x *AuthReply) Reset() { func (x *AuthReply) Reset() {
@ -241,6 +242,13 @@ func (x *AuthReply) GetUserinfo() *Userinfo {
return nil return nil
} }
func (x *AuthReply) GetPermissions() []byte {
if x != nil {
return x.Permissions
}
return nil
}
var File_auth_proto protoreflect.FileDescriptor var File_auth_proto protoreflect.FileDescriptor
var file_auth_proto_rawDesc = []byte{ var file_auth_proto_rawDesc = []byte{
@ -264,7 +272,7 @@ var file_auth_proto_rawDesc = []byte{
0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00,
0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x88, 0x01, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x88, 0x01,
0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f,
0x6b, 0x65, 0x6e, 0x22, 0xda, 0x01, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c, 0x6b, 0x65, 0x6e, 0x22, 0x91, 0x02, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c,
0x79, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0c, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0c,
0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01,
@ -275,15 +283,18 @@ var file_auth_proto_rawDesc = []byte{
0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66,
0x6f, 0x48, 0x02, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x88, 0x01, 0x01, 0x6f, 0x48, 0x02, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x88, 0x01, 0x01,
0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x12, 0x25, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18,
0x6e, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x03, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
0x6b, 0x65, 0x6e, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x61, 0x63, 0x63, 0x65,
0x32, 0x3e, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x36, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x66,
0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x75,
0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x70, 0x65, 0x72, 0x6d,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x3e, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12,
0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x36, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12,
0x74, 0x6f, 0x33, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68,
0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (

View File

@ -28,4 +28,5 @@ message AuthReply {
optional string access_token = 2; optional string access_token = 2;
optional string refresh_token = 3; optional string refresh_token = 3;
optional Userinfo userinfo = 4; optional Userinfo userinfo = 4;
optional bytes permissions = 5;
} }

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.3.0 // - protoc-gen-go-grpc v1.3.0
// - protoc v4.25.3 // - protoc v5.26.1
// source: auth.proto // source: auth.proto
package proto package proto

View File

@ -6,6 +6,7 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
"github.com/spf13/viper" "github.com/spf13/viper"
"gorm.io/datatypes"
) )
type Account struct { type Account struct {
@ -17,7 +18,7 @@ type Account struct {
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Banner string `json:"banner"` Banner string `json:"banner"`
ConfirmedAt *time.Time `json:"confirmed_at"` ConfirmedAt *time.Time `json:"confirmed_at"`
PowerLevel int `json:"power_level"` PermNodes datatypes.JSONMap `json:"perm_nodes"`
Profile AccountProfile `json:"profile"` Profile AccountProfile `json:"profile"`
PersonalPage AccountPage `json:"personal_page"` PersonalPage AccountPage `json:"personal_page"`

View File

@ -42,10 +42,11 @@ func authFunc(c *fiber.Ctx, overrides ...string) error {
} }
rtk := c.Cookies(services.CookieRefreshKey) rtk := c.Cookies(services.CookieRefreshKey)
if user, atk, rtk, err := services.Authenticate(token, rtk, 0); err == nil { if user, perms, atk, rtk, err := services.Authenticate(token, rtk, 0); err == nil {
if atk != token { if atk != token {
services.SetJwtCookieSet(c, atk, rtk) services.SetJwtCookieSet(c, atk, rtk)
} }
c.Locals("permissions", perms)
c.Locals("principal", user) c.Locals("principal", user)
return nil return nil
} else { } else {

View File

@ -42,6 +42,7 @@ func listenWebsocket(c *websocket.Conn) {
var req struct { var req struct {
RequestID string `json:"request_id"` RequestID string `json:"request_id"`
KeypairID string `json:"keypair_id"` KeypairID string `json:"keypair_id"`
Algorithm string `json:"algorithm"`
OwnerID uint `json:"owner_id"` OwnerID uint `json:"owner_id"`
Deadline int64 `json:"deadline"` Deadline int64 `json:"deadline"`
} }
@ -49,11 +50,12 @@ func listenWebsocket(c *websocket.Conn) {
if len(req.RequestID) <= 0 || len(req.KeypairID) <= 0 || req.OwnerID <= 0 { if len(req.RequestID) <= 0 || len(req.KeypairID) <= 0 || req.OwnerID <= 0 {
message = lo.ToPtr(models.UnifiedCommandFromError(fmt.Errorf("invalid request"))) message = lo.ToPtr(models.UnifiedCommandFromError(fmt.Errorf("invalid request")))
} }
services.KexRequest(c, req.RequestID, req.KeypairID, req.OwnerID, req.Deadline) services.KexRequest(c, req.RequestID, req.KeypairID, req.Algorithm, req.OwnerID, req.Deadline)
case "kex.provide": case "kex.provide":
var req struct { var req struct {
RequestID string `json:"request_id"` RequestID string `json:"request_id"`
KeypairID string `json:"keypair_id"` KeypairID string `json:"keypair_id"`
Algorithm string `json:"algorithm"`
PublicKey []byte `json:"public_key"` PublicKey []byte `json:"public_key"`
} }
_ = jsoniter.Unmarshal(payload, &req) _ = jsoniter.Unmarshal(payload, &req)

View File

@ -2,6 +2,8 @@ package services
import ( import (
"fmt" "fmt"
"github.com/spf13/viper"
"gorm.io/datatypes"
"time" "time"
"git.solsynth.dev/hydrogen/passport/pkg/database" "git.solsynth.dev/hydrogen/passport/pkg/database"
@ -66,7 +68,7 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
VerifiedAt: nil, VerifiedAt: nil,
}, },
}, },
PowerLevel: 0, PermNodes: datatypes.JSONMap(viper.GetStringMap("permissions.default")),
ConfirmedAt: nil, ConfirmedAt: nil,
} }
@ -98,7 +100,14 @@ func ConfirmAccount(code string) error {
return database.C.Transaction(func(tx *gorm.DB) error { return database.C.Transaction(func(tx *gorm.DB) error {
user.ConfirmedAt = lo.ToPtr(time.Now()) user.ConfirmedAt = lo.ToPtr(time.Now())
user.PowerLevel += 5
for k, v := range viper.GetStringMap("permissions.verified") {
if val, ok := user.PermNodes[k]; !ok {
user.PermNodes[k] = v
} else if !HasPermNode(val, v) {
user.PermNodes[k] = v
}
}
if err := database.C.Delete(&token).Error; err != nil { if err := database.C.Delete(&token).Error; err != nil {
return err return err

View File

@ -14,7 +14,7 @@ import (
const authContextBucket = "AuthContext" const authContextBucket = "AuthContext"
func Authenticate(access, refresh string, depth int) (user models.Account, newAccess, newRefresh string, err error) { func Authenticate(access, refresh string, depth int) (user models.Account, perms map[string]any, newAccess, newRefresh string, err error) {
var claims PayloadClaims var claims PayloadClaims
claims, err = DecodeJwt(access) claims, err = DecodeJwt(access)
if err != nil { if err != nil {
@ -37,6 +37,7 @@ func Authenticate(access, refresh string, depth int) (user models.Account, newAc
ctx, lookupErr := GetAuthContext(claims.ID) ctx, lookupErr := GetAuthContext(claims.ID)
if lookupErr == nil { if lookupErr == nil {
log.Debug().Str("jti", claims.ID).Msg("Hit auth context cache once!") log.Debug().Str("jti", claims.ID).Msg("Hit auth context cache once!")
perms = FilterPermNodes(ctx.Account.PermNodes, ctx.Ticket.Claims)
user = ctx.Account user = ctx.Account
return return
} }
@ -44,6 +45,7 @@ func Authenticate(access, refresh string, depth int) (user models.Account, newAc
ctx, err = GrantAuthContext(claims.ID) ctx, err = GrantAuthContext(claims.ID)
if err == nil { if err == nil {
log.Debug().Str("jti", claims.ID).Err(lookupErr).Msg("Missed auth context cache once!") log.Debug().Str("jti", claims.ID).Err(lookupErr).Msg("Missed auth context cache once!")
perms = FilterPermNodes(ctx.Account.PermNodes, ctx.Ticket.Claims)
user = ctx.Account user = ctx.Account
return return
} }
@ -97,7 +99,7 @@ func GrantAuthContext(jti string) (models.AuthContext, error) {
return ctx, fmt.Errorf("invalid account: %v", err) return ctx, fmt.Errorf("invalid account: %v", err)
} }
// Every context should expires in some while // Every context should expire in some while
// Once user update their account info, this will have delay to update // Once user update their account info, this will have delay to update
ctx = models.AuthContext{ ctx = models.AuthContext{
Ticket: ticket, Ticket: ticket,

View File

@ -15,7 +15,7 @@ type kexRequest struct {
var kexRequests = make(map[string]map[string]kexRequest) var kexRequests = make(map[string]map[string]kexRequest)
func KexRequest(conn *websocket.Conn, requestId, keypairId string, ownerId uint, deadline int64) { func KexRequest(conn *websocket.Conn, requestId, keypairId, algorithm string, ownerId uint, deadline int64) {
if kexRequests[keypairId] == nil { if kexRequests[keypairId] == nil {
kexRequests[keypairId] = make(map[string]kexRequest) kexRequests[keypairId] = make(map[string]kexRequest)
} }
@ -38,6 +38,7 @@ func KexRequest(conn *websocket.Conn, requestId, keypairId string, ownerId uint,
Payload: fiber.Map{ Payload: fiber.Map{
"request_id": requestId, "request_id": requestId,
"keypair_id": keypairId, "keypair_id": keypairId,
"algorithm": algorithm,
"owner_id": ownerId, "owner_id": ownerId,
"deadline": deadline, "deadline": deadline,
}, },

55
pkg/services/perms.go Normal file
View File

@ -0,0 +1,55 @@
package services
import (
"reflect"
"regexp"
"strings"
)
func HasPermNode(held any, required any) bool {
heldValue := reflect.ValueOf(held)
requiredValue := reflect.ValueOf(required)
switch heldValue.Kind() {
case reflect.Int, reflect.Float64:
if heldValue.Float() >= requiredValue.Float() {
return true
}
case reflect.String:
if heldValue.String() == requiredValue.String() {
return true
}
case reflect.Slice, reflect.Array:
for i := 0; i < heldValue.Len(); i++ {
if reflect.DeepEqual(heldValue.Index(i).Interface(), required) {
return true
}
}
default:
if reflect.DeepEqual(held, required) {
return true
}
}
return false
}
func FilterPermNodes(tree map[string]any, claims []string) map[string]any {
filteredTree := make(map[string]any)
match := func(claim, permission string) bool {
regex := strings.Replace(permission, "*", ".*", -1)
match, _ := regexp.MatchString("^"+regex+"$", claim)
return match
}
for _, claim := range claims {
for key, value := range tree {
if match(claim, key) {
filteredTree[key] = value
}
}
}
return filteredTree
}

View File

@ -35,3 +35,9 @@ refresh_token_duration = 2592000
dsn = "host=localhost dbname=hy_passport port=5432 sslmode=disable" dsn = "host=localhost dbname=hy_passport port=5432 sslmode=disable"
prefix = "passport_" prefix = "passport_"
bolt = "uploads/bolt.db" bolt = "uploads/bolt.db"
[permissions.default]
CreatePaperclipAttachments = 1048576
[permissions.verified]
CreatePaperclipAttachments = 26214400