Merge pull request #627 from butonic/add-basic-auth-option
add enable basic auth option and check permissions
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user