Merge pull request #627 from butonic/add-basic-auth-option

add enable basic auth option and check permissions
This commit is contained in:
Jörn Friedrich Dreyer
2020-11-05 16:36:16 +01:00
committed by GitHub
50 changed files with 1530 additions and 2286 deletions
@@ -630,7 +630,7 @@ func TestListAccounts(t *testing.T) {
assert.NoError(t, err)
assert.IsType(t, &proto.ListAccountsResponse{}, resp)
assert.Equal(t, 8, len(resp.Accounts))
assert.Equal(t, 9, len(resp.Accounts))
assertResponseContainsUser(t, resp, getAccount("user1"))
assertResponseContainsUser(t, resp, getAccount("user2"))
@@ -642,8 +642,8 @@ func TestListWithoutUserCreation(t *testing.T) {
resp, err := listAccounts(t)
assert.NoError(t, err)
// Only 5 default users
assert.Equal(t, 6, len(resp.Accounts))
// Only 7 default users
assert.Equal(t, 7, len(resp.Accounts))
cleanUp(t)
}
+82 -12
View File
@@ -16,8 +16,10 @@ import (
"github.com/golang/protobuf/ptypes/empty"
fieldmask_utils "github.com/mennanov/fieldmask-utils"
merrors "github.com/micro/go-micro/v2/errors"
"github.com/micro/go-micro/v2/metadata"
"github.com/owncloud/ocis/accounts/pkg/proto/v0"
"github.com/owncloud/ocis/accounts/pkg/storage"
"github.com/owncloud/ocis/ocis-pkg/middleware"
"github.com/owncloud/ocis/ocis-pkg/roles"
settings "github.com/owncloud/ocis/settings/pkg/proto/v0"
settings_svc "github.com/owncloud/ocis/settings/pkg/service/v0"
@@ -64,10 +66,14 @@ func (s Service) hasAccountManagementPermissions(ctx context.Context) bool {
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
if !ok {
/**
* FIXME: with this we are skipping permission checks on all requests that are coming in without roleIDs in the
* metadata context. This is a huge security impairment, as that's the case not only for grpc requests but also
* for unauthenticated http requests and http requests coming in without hitting the ocis-proxy first.
* FIXME: with this we are skipping permission checks on all requests that are coming in without roleIDs in the
* metadata context. This is a huge security impairment, as that's the case not only for grpc requests but also
* for unauthenticated http requests and http requests coming in without hitting the ocis-proxy first.
*/
// TODO add system role for internal requests.
// - at least the proxy needs to look up account info
// - glauth needs to make bind requests
// tracked as OCIS-454
return true
}
@@ -75,6 +81,17 @@ func (s Service) hasAccountManagementPermissions(ctx context.Context) bool {
return s.RoleManager.FindPermissionByID(ctx, roleIDs, AccountManagementPermissionID) != nil
}
func (s Service) hasSelfManagementPermissions(ctx context.Context) bool {
// get roles from context
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
if !ok {
return false
}
// check if permission is present in roles of the authenticated account
return s.RoleManager.FindPermissionByID(ctx, roleIDs, SelfManagementPermissionID) != nil
}
// serviceUserToIndex temporarily adds a service user to the index, which is supposed to be removed before the lock on the handler function is released
func (s Service) serviceUserToIndex() (teardownServiceUser func()) {
if s.Config.ServiceUser.Username != "" && s.Config.ServiceUser.UUID != "" {
@@ -105,9 +122,12 @@ func (s Service) getInMemoryServiceUser() proto.Account {
// ListAccounts implements the AccountsServiceHandler interface
// the query contains account properties
func (s Service) ListAccounts(ctx context.Context, in *proto.ListAccountsRequest, out *proto.ListAccountsResponse) (err error) {
if !s.hasAccountManagementPermissions(ctx) {
hasSelf := s.hasSelfManagementPermissions(ctx)
hasManagement := s.hasAccountManagementPermissions(ctx)
if !hasSelf && !hasManagement {
return merrors.Forbidden(s.id, "no permission for ListAccounts")
}
onlySelf := hasSelf && !hasManagement
accLock.Lock()
defer accLock.Unlock()
@@ -146,6 +166,15 @@ func (s Service) ListAccounts(ctx context.Context, in *proto.ListAccountsRequest
return nil
}
if onlySelf {
// limit list to own account id
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
in.Query = "id eq '" + aid + "'"
} else {
return merrors.InternalServerError(s.id, "account id not in context")
}
}
if in.Query == "" {
err = s.repo.LoadAccounts(ctx, &out.Accounts)
if err != nil {
@@ -202,9 +231,12 @@ func (s Service) findAccountsByQuery(ctx context.Context, query string) ([]strin
// GetAccount implements the AccountsServiceHandler interface
func (s Service) GetAccount(ctx context.Context, in *proto.GetAccountRequest, out *proto.Account) (err error) {
if !s.hasAccountManagementPermissions(ctx) {
hasSelf := s.hasSelfManagementPermissions(ctx)
hasManagement := s.hasAccountManagementPermissions(ctx)
if !hasSelf && !hasManagement {
return merrors.Forbidden(s.id, "no permission for GetAccount")
}
onlySelf := hasSelf && !hasManagement
accLock.Lock()
defer accLock.Unlock()
@@ -213,6 +245,17 @@ func (s Service) GetAccount(ctx context.Context, in *proto.GetAccountRequest, ou
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
if onlySelf {
// limit get to own account id
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
if id != aid {
return merrors.Forbidden(s.id, "no permission for GetAccount of another user")
}
} else {
return merrors.InternalServerError(s.id, "account id not in context")
}
}
if err = s.repo.LoadAccount(ctx, id, out); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "account not found: %v", err.Error())
@@ -268,7 +311,7 @@ func (s Service) CreateAccount(ctx context.Context, in *proto.CreateAccountReque
return merrors.InternalServerError(s.id, "could not check if account exists: %v", err.Error())
}
if exists {
return merrors.BadRequest(s.id, "account already exists")
return merrors.Conflict(s.id, "account already exists")
}
if out.PasswordProfile != nil {
@@ -298,7 +341,7 @@ func (s Service) CreateAccount(ctx context.Context, in *proto.CreateAccountReque
indexResults, err := s.index.Add(out)
if err != nil {
s.rollbackCreateAccount(ctx, out)
return merrors.BadRequest(s.id, "Account already exists %v", err.Error())
return merrors.Conflict(s.id, "Account already exists %v", err.Error())
}
s.log.Debug().Interface("account", out).Msg("account after indexing")
@@ -370,9 +413,12 @@ func (s Service) rollbackCreateAccount(ctx context.Context, acc *proto.Account)
// read only fields are ignored
// TODO how can we unset specific values? using the update mask
func (s Service) UpdateAccount(ctx context.Context, in *proto.UpdateAccountRequest, out *proto.Account) (err error) {
if !s.hasAccountManagementPermissions(ctx) {
hasSelf := s.hasSelfManagementPermissions(ctx)
hasManagement := s.hasAccountManagementPermissions(ctx)
if !hasSelf && !hasManagement {
return merrors.Forbidden(s.id, "no permission for UpdateAccount")
}
onlySelf := hasSelf && !hasManagement
accLock.Lock()
defer accLock.Unlock()
@@ -388,6 +434,17 @@ func (s Service) UpdateAccount(ctx context.Context, in *proto.UpdateAccountReque
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
if onlySelf {
// limit update to own account id
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
if id != aid {
return merrors.Forbidden(s.id, "no permission to UpdateAccount of another user")
}
} else {
return merrors.InternalServerError(s.id, "account id not in context")
}
}
if err = s.repo.LoadAccount(ctx, id, out); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "account not found: %v", err.Error())
@@ -395,7 +452,6 @@ func (s Service) UpdateAccount(ctx context.Context, in *proto.UpdateAccountReque
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
}
t := time.Now()
@@ -404,9 +460,15 @@ func (s Service) UpdateAccount(ctx context.Context, in *proto.UpdateAccountReque
Nanos: int32(t.Nanosecond()),
}
validMask, err := validateUpdate(in.UpdateMask, updatableAccountPaths)
if err != nil {
return merrors.BadRequest(s.id, "%s", err)
var validMask fieldmask_utils.FieldFilterContainer
if onlySelf {
if validMask, err = validateUpdate(in.UpdateMask, selfUpdatableAccountPaths); err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
} else {
if validMask, err = validateUpdate(in.UpdateMask, updatableAccountPaths); err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
}
if _, exists := validMask.Filter("PreferredName"); exists {
@@ -490,6 +552,14 @@ func (s Service) UpdateAccount(ctx context.Context, in *proto.UpdateAccountReque
return
}
// whitelist of all paths/fields which can be updated by users themself
var selfUpdatableAccountPaths = map[string]struct{}{
"DisplayName": {},
"Description": {},
"Mail": {}, // read only?,
"PasswordProfile.Password": {},
}
// whitelist of all paths/fields which can be updated by clients
var updatableAccountPaths = map[string]struct{}{
"AccountEnabled": {},
+30 -15
View File
@@ -50,25 +50,30 @@ func (s Service) deflateMembers(g *proto.Group) {
}
// ListGroups implements the GroupsServiceHandler interface
func (s Service) ListGroups(c context.Context, in *proto.ListGroupsRequest, out *proto.ListGroupsResponse) (err error) {
var searchResults []string
out.Groups = make([]*proto.Group, 0)
func (s Service) ListGroups(ctx context.Context, in *proto.ListGroupsRequest, out *proto.ListGroupsResponse) (err error) {
if in.Query == "" {
searchResults, _ = s.index.FindByPartial(&proto.Group{}, "DisplayName", "*")
err = s.repo.LoadGroups(ctx, &out.Groups)
if err != nil {
s.log.Err(err).Msg("failed to load all groups from storage")
return merrors.InternalServerError(s.id, "failed to load all groups")
}
for i := range out.Groups {
a := out.Groups[i]
// TODO add accounts only if requested
// if in.FieldMask ...
s.expandMembers(a)
}
return nil
}
/*
var startsWithIDQuery = regexp.MustCompile(`^startswith\(id,'(.*)'\)$`)
match := startsWithIDQuery.FindStringSubmatch(in.Query)
if len(match) == 2 {
searchResults = []string{match[1]}
}
*/
searchResults, err := s.findGroupsByQuery(ctx, in.Query)
out.Groups = make([]*proto.Group, 0, len(searchResults))
for _, hit := range searchResults {
g := &proto.Group{}
if err = s.repo.LoadGroup(c, hit, g); err != nil {
if err = s.repo.LoadGroup(ctx, hit, g); err != nil {
s.log.Error().Err(err).Str("group", hit).Msg("could not load group, skipping")
continue
}
@@ -83,6 +88,9 @@ func (s Service) ListGroups(c context.Context, in *proto.ListGroupsRequest, out
return
}
func (s Service) findGroupsByQuery(ctx context.Context, query string) ([]string, error) {
return s.index.Query(&proto.Group{}, query)
}
// GetGroup implements the GroupsServiceHandler interface
func (s Service) GetGroup(c context.Context, in *proto.GetGroupRequest, out *proto.Group) (err error) {
@@ -249,8 +257,11 @@ func (s Service) AddMember(c context.Context, in *proto.AddMemberRequest, out *p
alreadyRelated = true
}
}
aref := &proto.Account{
Id: a.Id,
}
if !alreadyRelated {
g.Members = append(g.Members, a)
g.Members = append(g.Members, aref)
}
// check if we need to add the group to the account
@@ -261,8 +272,12 @@ func (s Service) AddMember(c context.Context, in *proto.AddMemberRequest, out *p
break
}
}
// only store the reference to prevent recurision when marshaling json
gref := &proto.Group{
Id: g.Id,
}
if !alreadyRelated {
a.MemberOf = append(a.MemberOf, g)
a.MemberOf = append(a.MemberOf, gref)
}
if err = s.repo.WriteAccount(c, a); err != nil {
+26 -3
View File
@@ -11,13 +11,17 @@ import (
const (
// AccountManagementPermissionID is the hardcoded setting UUID for the account management permission
AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
// AccountManagementPermissionName is the hardcoded setting name for the account management permission
AccountManagementPermissionName string = "account-management"
// GroupManagementPermissionID is the hardcoded setting UUID for the group management permission
GroupManagementPermissionID string = "522adfbe-5908-45b4-b135-41979de73245"
GroupManagementPermissionID string = "522adfbe-5908-45b4-b135-41979de73245"
// GroupManagementPermissionName is the hardcoded setting name for the group management permission
GroupManagementPermissionName string = "group-management"
GroupManagementPermissionName string = "group-management"
// SelfManagementPermissionID is the hardcoded setting UUID for the self management permission
SelfManagementPermissionID string = "e03070e9-4362-4cc6-a872-1c7cb2eb2b8e"
// SelfManagementPermissionName is the hardcoded setting name for the self management permission
SelfManagementPermissionName string = "self-management"
)
// RegisterPermissions registers permissions for account management and group management with the settings service.
@@ -78,5 +82,24 @@ func generateAccountManagementPermissionsRequests() []settings.AddSettingToBundl
},
},
},
{
BundleId: ssvc.BundleUUIDRoleUser,
Setting: &settings.Setting{
Id: SelfManagementPermissionID,
Name: SelfManagementPermissionName,
DisplayName: "Self Management",
Description: "This permission gives access to self management.",
Resource: &settings.Resource{
Type: settings.Resource_TYPE_USER,
Id: "me",
},
Value: &settings.Setting_PermissionValue{
PermissionValue: &settings.Permission{
Operation: settings.Permission_OPERATION_READWRITE,
Constraint: settings.Permission_CONSTRAINT_OWN,
},
},
},
},
}
}
+19
View File
@@ -218,6 +218,22 @@ func (s Service) createDefaultAccounts() (err error) {
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
},
},
{
Id: "ddc2004c-0977-11eb-9d3f-a793888cd0f8",
PreferredName: "admin",
OnPremisesSamAccountName: "admin",
Mail: "admin@example.org",
DisplayName: "Admin",
UidNumber: 20004,
GidNumber: 30000,
PasswordProfile: &proto.PasswordProfile{
Password: "$6$rounds=95551$/bdqsmiGleA20kAS$rCAvHV7wjaHVF5nEVAnpW7mugRqcnPmdU4UPqhSroE74gXFxNGZflCF.ZyHwocDwgAw3uLkqsCzB1h5bXBjYB0",
},
AccountEnabled: true,
MemberOf: []*proto.Group{
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
},
},
// technical users for kopano and reva
{
Id: "820ba2a1-3f54-4538-80a4-2d73007e30bf",
@@ -293,6 +309,9 @@ func (s Service) createDefaultAccounts() (err error) {
assignRoleToUser("058bff95-6708-4fe5-91e4-9ea3d377588b", settings_svc.BundleUUIDRoleAdmin, s.RoleService, s.log)
for _, accountID := range []string{
"058bff95-6708-4fe5-91e4-9ea3d377588b", //moss
"ddc2004c-0977-11eb-9d3f-a793888cd0f8", //admin
"820ba2a1-3f54-4538-80a4-2d73007e30bf", //konnectd
"bc596f3c-c955-4328-80a0-60d018b4ad57", //reva
} {
assignRoleToUser(accountID, settings_svc.BundleUUIDRoleAdmin, s.RoleService, s.log)
}
+18 -10
View File
@@ -73,15 +73,19 @@ func (r DiskRepo) LoadAccount(ctx context.Context, id string, a *proto.Account)
// LoadAccounts loads all the accounts from the local filesystem
func (r DiskRepo) LoadAccounts(ctx context.Context, a *[]*proto.Account) (err error) {
root := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder)
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
infos, err := ioutil.ReadDir(root)
if err != nil {
return err
}
for i := range infos {
acc := &proto.Account{}
if e := r.LoadAccount(ctx, filepath.Base(path), acc); e != nil {
if e := r.LoadAccount(ctx, infos[i].Name(), acc); e != nil {
r.log.Err(e).Msg("could not load account")
return nil
continue
}
*a = append(*a, acc)
return nil
})
}
return nil
}
// DeleteAccount from the local filesystem
@@ -135,15 +139,19 @@ func (r DiskRepo) LoadGroup(ctx context.Context, id string, g *proto.Group) (err
// LoadGroups loads all the groups from the local filesystem
func (r DiskRepo) LoadGroups(ctx context.Context, g *[]*proto.Group) (err error) {
root := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder)
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
infos, err := ioutil.ReadDir(root)
if err != nil {
return err
}
for i := range infos {
grp := &proto.Group{}
if e := r.LoadGroup(ctx, filepath.Base(path), grp); e != nil {
if e := r.LoadGroup(ctx, infos[i].Name(), grp); e != nil {
r.log.Err(e).Msg("could not load group")
return nil
continue
}
*g = append(*g, grp)
return nil
})
}
return nil
}
// DeleteGroup from the local filesystem