diff --git a/api/messaging/spec.json b/api/messaging/spec.json index 8ea835fa4..5549e9d5d 100644 --- a/api/messaging/spec.json +++ b/api/messaging/spec.json @@ -718,12 +718,6 @@ } ], "get": [ - { - "name": "scope", - "type": "string", - "required": true, - "title": "Permissions scope" - }, { "name": "resource", "type": "string", @@ -756,6 +750,22 @@ } ] } + }, + { + "name": "scopes", + "path": "/permissions/scopes/{scope}", + "method": "GET", + "title": "List resources for given scope", + "parameters": { + "path": [ + { + "name": "scope", + "type": "string", + "required": true, + "title": "Scope" + } + ] + } } ] } diff --git a/api/messaging/spec/permissions.json b/api/messaging/spec/permissions.json index 63b8eea09..1c2033b9c 100644 --- a/api/messaging/spec/permissions.json +++ b/api/messaging/spec/permissions.json @@ -30,12 +30,6 @@ "Path": "/permissions/{teamID}", "Parameters": { "get": [ - { - "name": "scope", - "required": true, - "title": "Permissions scope", - "type": "string" - }, { "name": "resource", "required": true, @@ -76,6 +70,22 @@ } ] } + }, + { + "Name": "scopes", + "Method": "GET", + "Title": "List resources for given scope", + "Path": "/permissions/scopes/{scope}", + "Parameters": { + "path": [ + { + "name": "scope", + "required": true, + "title": "Scope", + "type": "string" + } + ] + } } ] } \ No newline at end of file diff --git a/docs/messaging/README.md b/docs/messaging/README.md index 5daeec74d..1697902bb 100644 --- a/docs/messaging/README.md +++ b/docs/messaging/README.md @@ -475,7 +475,6 @@ The following event types may be sent with a message event: | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | -| scope | string | GET | Permissions scope | N/A | YES | | resource | string | GET | Permissions resource | N/A | YES | | teamID | uint64 | PATH | Team ID | N/A | YES | @@ -494,6 +493,20 @@ The following event types may be sent with a message event: | teamID | uint64 | PATH | Team ID | N/A | YES | | permissions | []rules.Rules | POST | List of rules to set | N/A | YES | +## List resources for given scope + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/permissions/permissions/scopes/{scope}` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| scope | string | PATH | Scope | N/A | YES | + diff --git a/internal/organization/organization.go b/internal/organization/organization.go index 062953c36..e310da78b 100644 --- a/internal/organization/organization.go +++ b/internal/organization/organization.go @@ -6,10 +6,14 @@ import ( "github.com/crusttech/crust/system/types" ) +func Crust() types.Organisation { + return types.Organisation{ID: 1} +} + func GetFromContext(ctx context.Context) types.Organisation { if orgID, ok := ctx.Value("organizationID").(uint64); ok { return types.Organisation{ID: orgID} } else { - return types.Organisation{ID: 1} + return Crust() } } diff --git a/messaging/rest/handlers/permissions.go b/messaging/rest/handlers/permissions.go index 25f6d6812..2c7960f83 100644 --- a/messaging/rest/handlers/permissions.go +++ b/messaging/rest/handlers/permissions.go @@ -30,13 +30,15 @@ type PermissionsAPI interface { List(context.Context, *request.PermissionsList) (interface{}, error) Get(context.Context, *request.PermissionsGet) (interface{}, error) Set(context.Context, *request.PermissionsSet) (interface{}, error) + Scopes(context.Context, *request.PermissionsScopes) (interface{}, error) } // HTTP API interface type Permissions struct { - List func(http.ResponseWriter, *http.Request) - Get func(http.ResponseWriter, *http.Request) - Set func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Get func(http.ResponseWriter, *http.Request) + Set func(http.ResponseWriter, *http.Request) + Scopes func(http.ResponseWriter, *http.Request) } func NewPermissions(ph PermissionsAPI) *Permissions { @@ -62,6 +64,13 @@ func NewPermissions(ph PermissionsAPI) *Permissions { return ph.Set(r.Context(), params) }) }, + Scopes: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewPermissionsScopes() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return ph.Scopes(r.Context(), params) + }) + }, } } @@ -72,6 +81,7 @@ func (ph *Permissions) MountRoutes(r chi.Router, middlewares ...func(http.Handle r.Get("/permissions", ph.List) r.Get("/permissions/{teamID}", ph.Get) r.Post("/permissions/{teamID}", ph.Set) + r.Get("/permissions/scopes/{scope}", ph.Scopes) }) }) } diff --git a/messaging/rest/permissions.go b/messaging/rest/permissions.go index 45efeefe7..a43490ed4 100644 --- a/messaging/rest/permissions.go +++ b/messaging/rest/permissions.go @@ -25,9 +25,13 @@ func (ctrl *Permissions) List(ctx context.Context, r *request.PermissionsList) ( } func (ctrl *Permissions) Get(ctx context.Context, r *request.PermissionsGet) (interface{}, error) { - return ctrl.svc.perms.Get(r.TeamID, r.Scope, r.Resource) + return ctrl.svc.perms.Get(r.TeamID, r.Resource) } -func (ctrl *Permissions) SetTeam(ctx context.Context, r *request.PermissionsSet) (interface{}, error) { +func (ctrl *Permissions) Set(ctx context.Context, r *request.PermissionsSet) (interface{}, error) { return ctrl.svc.perms.Set(r.TeamID, r.Permissions) } + +func (ctrl *Permissions) Scopes(ctx context.Context, r *request.PermissionsScopes) (interface{}, error) { + return ctrl.svc.perms.Scopes(r.Scope) +} diff --git a/messaging/rest/request/permissions.go b/messaging/rest/request/permissions.go index 1aebb3c9f..31ab63773 100644 --- a/messaging/rest/request/permissions.go +++ b/messaging/rest/request/permissions.go @@ -73,7 +73,6 @@ var _ RequestFiller = NewPermissionsList() // Permissions get request parameters type PermissionsGet struct { - Scope string Resource string TeamID uint64 `json:",string"` } @@ -109,10 +108,6 @@ func (p *PermissionsGet) Fill(r *http.Request) (err error) { post[name] = string(param[0]) } - if val, ok := get["scope"]; ok { - - p.Scope = val - } if val, ok := get["resource"]; ok { p.Resource = val @@ -167,3 +162,46 @@ func (p *PermissionsSet) Fill(r *http.Request) (err error) { } var _ RequestFiller = NewPermissionsSet() + +// Permissions scopes request parameters +type PermissionsScopes struct { + Scope string +} + +func NewPermissionsScopes() *PermissionsScopes { + return &PermissionsScopes{} +} + +func (p *PermissionsScopes) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(p) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + p.Scope = chi.URLParam(r, "scope") + + return err +} + +var _ RequestFiller = NewPermissionsScopes() diff --git a/messaging/rest/router.go b/messaging/rest/router.go index 10953ef28..223f92e74 100644 --- a/messaging/rest/router.go +++ b/messaging/rest/router.go @@ -22,6 +22,7 @@ func MountRoutes() func(chi.Router) { handlers.NewChannel(Channel{}.New()).MountRoutes(r) handlers.NewMessage(Message{}.New()).MountRoutes(r) handlers.NewSearch(Search{}.New()).MountRoutes(r) + handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) }) } } diff --git a/messaging/service/main_test.go b/messaging/service/main_test.go index 44db976fe..d1f47c57c 100644 --- a/messaging/service/main_test.go +++ b/messaging/service/main_test.go @@ -2,14 +2,64 @@ package service import ( "fmt" + "log" + "os" "runtime" "testing" + + "github.com/joho/godotenv" + "github.com/namsral/flag" + "github.com/titpetric/factory" + + systemMigrate "github.com/crusttech/crust/system/db" ) type mockDB struct{} func (mockDB) Transaction(callback func() error) error { return callback() } +func TestMain(m *testing.M) { + // @todo this is a very optimistic initialization, make it more robust + godotenv.Load("../../.env") + + prefix := "messaging" + dsn := "" + + p := func(s string) string { + return prefix + "-" + s + } + + flag.StringVar(&dsn, p("db-dsn"), "crust:crust@tcp(db1:3306)/crust?collation=utf8mb4_general_ci", "DSN for database connection") + flag.Parse() + + if testing.Short() { + return + } + + factory.Database.Add("default", dsn) + + db := factory.Database.MustGet() + db.Profiler = &factory.Database.ProfilerStdout + + // migrate database schema + if err := systemMigrate.Migrate(db); err != nil { + log.Printf("Error running migrations: %+v\n", err) + return + } + + // clean up tables + { + for _, name := range []string{"sys_user", "sys_team", "sys_team_member", "sys_organisation", "sys_rules"} { + _, err := db.Exec("truncate " + name) + if err != nil { + panic("Error when clearing " + name + ": " + err.Error()) + } + } + } + + os.Exit(m.Run()) +} + func assert(t *testing.T, ok bool, format string, args ...interface{}) bool { if !ok { _, file, line, _ := runtime.Caller(1) diff --git a/messaging/service/permissions.go b/messaging/service/permissions.go index 78b333bf5..39e26027e 100644 --- a/messaging/service/permissions.go +++ b/messaging/service/permissions.go @@ -5,8 +5,11 @@ import ( "github.com/pkg/errors" + "github.com/crusttech/crust/internal/organization" internalRules "github.com/crusttech/crust/internal/rules" "github.com/crusttech/crust/messaging/repository" + "github.com/crusttech/crust/messaging/types" + systemRepository "github.com/crusttech/crust/system/repository" ) type ( @@ -14,15 +17,21 @@ type ( db db ctx context.Context - scopes internalRules.ScopeInterface + team systemRepository.TeamRepository + channel repository.ChannelRepository + + scopes internalRules.ScopeInterface + resources internalRules.ResourcesInterface } PermissionsService interface { With(ctx context.Context) PermissionsService List() (interface{}, error) - Get(teamID uint64, scope string, resource string) (interface{}, error) + Get(teamID uint64, resource string) (interface{}, error) Set(teamID uint64, rules []internalRules.Rules) (interface{}, error) + + Scopes(scope string) (interface{}, error) } ) @@ -32,12 +41,17 @@ func Permissions(scopes internalRules.ScopeInterface) PermissionsService { }).With(context.Background()) } -func (svc *permissions) With(ctx context.Context) PermissionsService { +func (p *permissions) With(ctx context.Context) PermissionsService { db := repository.DB(ctx) return &permissions{ - db: db, - ctx: ctx, - scopes: svc.scopes, + db: db, + ctx: ctx, + + team: systemRepository.Team(ctx, db), + channel: repository.Channel(ctx, db), + + scopes: p.scopes, + resources: internalRules.NewResources(ctx, db), } } @@ -45,10 +59,41 @@ func (p *permissions) List() (interface{}, error) { return p.scopes.List(), nil } -func (p *permissions) Get(teamID uint64, scope string, resource string) (interface{}, error) { - return nil, errors.New("service.permissions.get: not implemented") +func (p *permissions) Get(teamID uint64, resource string) (interface{}, error) { + return p.resources.ListGrants(teamID, resource) } func (p *permissions) Set(teamID uint64, rules []internalRules.Rules) (interface{}, error) { - return nil, errors.New("service.permissions.set: not implemented") + var err error + for _, rule := range rules { + err = p.resources.Grant( + teamID, + rule.Resource, + []string{rule.Operation}, + rule.Value, + ) + if err != nil { + break + } + } + return nil, err +} + +func (p *permissions) Scopes(scope string) (interface{}, error) { + switch scope { + case "organization": + // @todo organizations from DB once multi-org + // return p.organizaion.Find(nil) + orgs := []types.Organisation{ + types.Organisation{ + organization.Crust(), + }, + } + return orgs, nil + case "team": + return p.team.Find(nil) + case "channel": + return p.channel.FindChannels(nil) + } + return nil, errors.New("no scope defined") } diff --git a/messaging/service/permissions_test.go b/messaging/service/permissions_test.go new file mode 100644 index 000000000..2b14b2e58 --- /dev/null +++ b/messaging/service/permissions_test.go @@ -0,0 +1,119 @@ +package service + +import ( + "context" + "testing" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/internal/auth" + internalRules "github.com/crusttech/crust/internal/rules" + . "github.com/crusttech/crust/internal/test" + "github.com/crusttech/crust/messaging/types" + systemRepos "github.com/crusttech/crust/system/repository" + systemTypes "github.com/crusttech/crust/system/types" +) + +func TestPermissions(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + return + } + ctx := context.TODO() + + // Create user for test. + userRepo := systemRepos.User(ctx, factory.Database.MustGet()) + user := &systemTypes.User{ + Name: "John Doe", + Username: "johndoe", + SatosaID: "1234", + } + err := user.GeneratePassword("johndoe") + NoError(t, err, "expected no error generating password, got %v", err) + + _, err = userRepo.Create(user) + NoError(t, err, "expected no error creating user, got %v", err) + + // Create team for test and add user + teamRepo := systemRepos.Team(ctx, factory.Database.MustGet()) + team := &systemTypes.Team{ + Name: "Test team v1", + } + _, err = teamRepo.Create(team) + NoError(t, err, "expected no error creating team, got %v", err) + + err = teamRepo.MemberAddByID(team.ID, user.ID) + NoError(t, err, "expected no error adding user to team, got %v", err) + + // Set Identity. + ctx = auth.SetIdentityToContext(ctx, user) + + // Create scopes. + scopes := internalRules.NewScope() + scopes.Add(&types.Organisation{}) + scopes.Add(&types.Team{}) + scopes.Add(&types.Channel{}) + + permissionsSvc := Permissions(scopes).With(ctx) + + // Get all available scopes and items + list, err := permissionsSvc.List() + + scopeItems := list.([]internalRules.ScopeItem) + NoError(t, err, "expected no error, receiving scopes") + Assert(t, len(scopeItems) == 3, "expected 3 scopes, got %v", len(scopeItems)) + + // Setup nothing for organization + organisationScope := scopeItems[0] + Assert(t, organisationScope.Scope == "organisation", "expected scope 'organisation', got %s", organisationScope.Scope) + + // Setup everything allow for team + teamScope := scopeItems[1] + Assert(t, teamScope.Scope == "team", "expected scope 'team', got %s", teamScope.Scope) + + rules := make([]internalRules.Rules, 0) + for _, group := range teamScope.Permissions { + for _, op := range group.Operations { + r := internalRules.Rules{ + TeamID: team.ID, + Resource: "team:1", + Operation: op.Key, + Value: internalRules.Allow, + } + rules = append(rules, r) + } + } + _, err = permissionsSvc.Set(team.ID, rules) + NoError(t, err, "expected no error, setting rules") + + // Deny all permissions for scope channel:1 + channelScope := scopeItems[2] + Assert(t, channelScope.Scope == "channel", "expected scope 'channel', got %s", channelScope.Scope) + + rules = make([]internalRules.Rules, 0) + for _, group := range channelScope.Permissions { + for _, op := range group.Operations { + r := internalRules.Rules{ + TeamID: team.ID, + Resource: "channel:1", + Operation: op.Key, + Value: internalRules.Deny, + } + rules = append(rules, r) + } + } + _, err = permissionsSvc.Set(team.ID, rules) + NoError(t, err, "expected no error, setting rules") + + // Check permission on channel and team level to test inheritance. + rls := Rules().With(ctx) + + canManageChannels := rls.canManageChannels() + Assert(t, canManageChannels == false, "expected canManageChannels == false, got %v", canManageChannels) + + canSendMessage := rls.canSendMessages(&types.Channel{ID: 1}) + Assert(t, canSendMessage == false, "expected canSendMessage == false, got %v", canSendMessage) + + canSendMessage = rls.canSendMessages(&types.Channel{ID: 2}) + Assert(t, canSendMessage == true, "expected canSendMessage == true, got %v", canSendMessage) +}