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
+3 -2
View File
@@ -362,7 +362,7 @@ def localApiTests(ctx, coreBranch = 'master', coreCommit = '', storage = 'ownclo
'OCIS_SKELETON_STRATEGY': '%s' % ('copy' if storage == 'owncloud' else 'upload'),
'TEST_OCIS':'true',
'BEHAT_FILTER_TAGS': '~@skipOnOcis-%s-Storage' % ('OC' if storage == 'owncloud' else 'OCIS'),
'PATH_TO_CORE': '/srv/app/testrunner'
'PATH_TO_CORE': '/srv/app/testrunner',
},
'commands': [
'cd ocis',
@@ -419,7 +419,7 @@ def coreApiTests(ctx, coreBranch = 'master', coreCommit = '', part_number = 1, n
'BEHAT_FILTER_TAGS': '~@notToImplementOnOCIS&&~@toImplementOnOCIS&&~comments-app-required&&~@federation-app-required&&~@notifications-app-required&&~systemtags-app-required&&~@local_storage&&~@skipOnOcis-%s-Storage' % ('OC' if storage == 'owncloud' else 'OCIS'),
'DIVIDE_INTO_NUM_PARTS': number_of_parts,
'RUN_PART': part_number,
'EXPECTED_FAILURES_FILE': '/drone/src/ocis/tests/acceptance/expected-failures-on-%s-storage.txt' % (storage.upper())
'EXPECTED_FAILURES_FILE': '/drone/src/ocis/tests/acceptance/expected-failures-on-%s-storage.txt' % (storage.upper()),
},
'commands': [
'cd /srv/app/testrunner',
@@ -1406,6 +1406,7 @@ def ocisServer(storage):
'STORAGE_DATAGATEWAY_PUBLIC_URL': 'https://ocis-server:9200/data',
'STORAGE_USERS_DATA_SERVER_URL': 'http://ocis-server:9158/data',
'STORAGE_FRONTEND_PUBLIC_URL': 'https://ocis-server:9200',
'PROXY_ENABLE_BASIC_AUTH': True,
'PHOENIX_WEB_CONFIG': '/drone/src/ocis/tests/config/drone/ocis-config.json',
'KONNECTD_IDENTIFIER_REGISTRATION_CONF': '/drone/src/ocis/tests/config/drone/identifier-registration.yml',
'KONNECTD_ISS': 'https://ocis-server:9200',
@@ -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
@@ -18,11 +18,13 @@ Feature: Accounts
When the user reloads the current page of the webUI
Then the displayed role of user "einstein" should be "Admin" on the WebUI
@skip @issue-product-167
Scenario: regular user should not be able to see accounts list
Given user "Marie" has logged in using the webUI
When the user browses to the accounts page
Then the user should not be able to see the accounts list on the WebUI
@skip @issue-product-167
Scenario: guest user should not be able to see accounts list
Given user "Moss" has logged in using the webUI
When the user browses to the accounts page
@@ -0,0 +1,6 @@
Enhancement: Add basic auth option
We added a new `enable-basic-auth` option and `PROXY_ENABLE_BASIC_AUTH` environment variable that can be set to `true` to make the proxy verify the basic auth header with the accounts service. This should only be used for testing and development and is disabled by default.
https://github.com/owncloud/ocis/pull/627
https://github.com/owncloud/product/issues/198
+3 -1
View File
@@ -29,9 +29,11 @@ File versions need a redis server. Start one with docker by using:
To start ocis:
```
bin/ocis server
PROXY_ENABLE_BASIC_AUTH=true bin/ocis server
```
`PROXY_ENABLE_BASIC_AUTH` will allow the acceptance tests to make requests against the provisioning api (and other endpoints) using basic auth.
### Run the acceptance tests
First we will need to clone the testing app in owncloud which contains the skeleton files required for running the tests.
In the ownCloud 10 core clone the testing app with the following command:
+2 -1
View File
@@ -91,9 +91,10 @@ marie:radioactivity
richard:superfluidity
```
There is an admin demo account:
There are admin demo accounts:
```console
moss:vista
admin:admin
```
## Runtime
+2
View File
@@ -55,6 +55,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzS
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CiscoM31/godata v0.0.0-20201003040028-eadcd34e7f06 h1:FKxVU/j9Dd8Je0YkVkm8Fxpz9zIeN21SEkcbzA6NWgY=
github.com/CiscoM31/godata v0.0.0-20201003040028-eadcd34e7f06/go.mod h1:tjaihnMBH6p5DVnGBksDQQHpErbrLvb9ek6cEWuyc7E=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e h1:Bqtt5C+uVk+vH/t5dmB47uDCTwxw16EYHqvJnmY2aQc=
github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e/go.mod h1:njRCDrl+1RQ/A/+KVU8Ho2EWAxUSkohOWczdW3dzDG0=
+1
View File
@@ -200,6 +200,7 @@ func Server(cfg *config.Config) *cli.Command {
glauth.LDAPS(&lscfg),
glauth.Backend(&bcfg),
glauth.Fallback(&fcfg),
glauth.RoleBundleUUID(cfg.RoleBundleUUID),
)
if err != nil {
+11 -10
View File
@@ -58,16 +58,17 @@ type Backend struct {
// Config combines all available configuration parts.
type Config struct {
File string
Log Log
Debug Debug
HTTP HTTP
Tracing Tracing
Ldap Ldap
Ldaps Ldaps
Backend Backend
Fallback Backend
Version string
File string
Log Log
Debug Debug
HTTP HTTP
Tracing Tracing
Ldap Ldap
Ldaps Ldaps
Backend Backend
Fallback Backend
Version string
RoleBundleUUID string
}
// New initializes a new configuration with or without defaults.
+7
View File
@@ -115,6 +115,13 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
EnvVars: []string{"GLAUTH_DEBUG_ZPAGES"},
Destination: &cfg.Debug.Zpages,
},
&cli.StringFlag{
Name: "role-bundle-id",
Value: "71881883-1768-46bd-a24d-a356a2afdf7f", // BundleUUIDRoleAdmin
Usage: "roleid used to make internal grpc requests",
EnvVars: []string{"GLAUTH_ROLE_BUNDLE_ID"},
Destination: &cfg.RoleBundleUUID,
},
&cli.StringFlag{
Name: "ldap-addr",
+39 -3
View File
@@ -2,6 +2,7 @@ package glauth
import (
"context"
"encoding/json"
"fmt"
"net"
"strconv"
@@ -9,10 +10,12 @@ import (
"github.com/glauth/glauth/pkg/handler"
"github.com/glauth/glauth/pkg/stats"
"github.com/micro/go-micro/v2/metadata"
ber "github.com/nmcclain/asn1-ber"
"github.com/nmcclain/ldap"
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/middleware"
)
type queryType string
@@ -29,6 +32,7 @@ type ocisHandler struct {
basedn string
nameFormat string
groupFormat string
rbid string
}
func (h ocisHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) {
@@ -66,8 +70,22 @@ func (h ocisHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAP
}
userName := strings.TrimPrefix(parts[0], "cn=")
// TODO make glauth context aware
ctx := context.Background()
// use a session with the bound user?
roleIDs, err := json.Marshal([]string{h.rbid})
if err != nil {
h.log.Error().
Err(err).
Str("handler", "ocis").
Msg("could not marshal roleid json")
return ldap.LDAPResultOperationsError, nil
}
ctx = metadata.Set(ctx, middleware.RoleIDs, string(roleIDs))
// check password
res, err := h.as.ListAccounts(context.TODO(), &accounts.ListAccountsRequest{
res, err := h.as.ListAccounts(ctx, &accounts.ListAccountsRequest{
//Query: fmt.Sprintf("username eq '%s'", username),
// TODO this allows lookung up users when you know the username using basic auth
// adding the password to the query is an option but sending the sover the wira a la scim seems ugly
@@ -76,6 +94,7 @@ func (h ocisHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAP
})
if err != nil || len(res.Accounts) == 0 {
h.log.Error().
Err(err).
Str("handler", "ocis").
Str("username", userName).
Str("binddn", bindDN).
@@ -162,6 +181,22 @@ func (h ocisHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn ne
}
}
// TODO make glauth context aware
ctx := context.Background()
// use a session with the bound user?
roleIDs, err := json.Marshal([]string{h.rbid})
if err != nil {
h.log.Error().
Err(err).
Str("handler", "ocis").
Msg("could not marshal roleid json")
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultOperationsError,
}, nil
}
ctx = metadata.Set(ctx, middleware.RoleIDs, string(roleIDs))
entries := []*ldap.Entry{}
h.log.Debug().
Str("handler", "ocis").
@@ -173,7 +208,7 @@ func (h ocisHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn ne
Msg("parsed query")
switch qtype {
case usersQuery:
accounts, err := h.as.ListAccounts(context.TODO(), &accounts.ListAccountsRequest{
accounts, err := h.as.ListAccounts(ctx, &accounts.ListAccountsRequest{
Query: query,
})
if err != nil {
@@ -193,7 +228,7 @@ func (h ocisHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn ne
}
entries = append(entries, h.mapAccounts(accounts.Accounts)...)
case groupsQuery:
groups, err := h.gs.ListGroups(context.TODO(), &accounts.ListGroupsRequest{
groups, err := h.gs.ListGroups(ctx, &accounts.ListGroupsRequest{
Query: query,
})
if err != nil {
@@ -501,6 +536,7 @@ func NewOCISHandler(opts ...Option) handler.Handler {
basedn: options.BaseDN,
nameFormat: options.NameFormat,
groupFormat: options.GroupFormat,
rbid: options.RoleBundleUUID,
}
return handler
}
+8
View File
@@ -22,6 +22,7 @@ type Options struct {
BaseDN string
NameFormat string
GroupFormat string
RoleBundleUUID string
AccountsService accounts.AccountsService
GroupsService accounts.GroupsService
}
@@ -113,3 +114,10 @@ func GroupsService(val accounts.GroupsService) Option {
o.GroupsService = val
}
}
// RoleBundleUUID provides a role bundle UUID to make internal grpc requests.
func RoleBundleUUID(val string) Option {
return func(o *Options) {
o.RoleBundleUUID = val
}
}
+2
View File
@@ -78,6 +78,7 @@ func Server(opts ...Option) (*LdapSvc, error) {
BaseDN(s.backend.Backend.BaseDN),
NameFormat(s.backend.Backend.NameFormat),
GroupFormat(s.backend.Backend.GroupFormat),
RoleBundleUUID(options.RoleBundleUUID),
)
default:
return nil, fmt.Errorf("unsupported backend %s - must be 'ldap', 'owncloud' or 'accounts'", s.backend.Backend.Datastore)
@@ -115,6 +116,7 @@ func Server(opts ...Option) (*LdapSvc, error) {
BaseDN(s.fallback.Backend.BaseDN),
NameFormat(s.fallback.Backend.NameFormat),
GroupFormat(s.fallback.Backend.GroupFormat),
RoleBundleUUID(options.RoleBundleUUID),
)
default:
return nil, fmt.Errorf("unsupported fallback %s - must be 'ldap', 'owncloud' or 'accounts'", s.fallback.Backend.Datastore)
+3
View File
@@ -196,6 +196,9 @@ func (k Konnectd) Index() http.HandlerFunc {
if err != nil {
k.logger.Fatal().Err(err).Msg("Could not read index template")
}
if err = f.Close(); err != nil {
k.logger.Fatal().Err(err).Msg("Could not close body")
}
// TODO add environment variable to make the path prefix configurable
pp := "/signin/v1"
+8 -4
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"github.com/cs3org/reva/pkg/token/manager/jwt"
"github.com/cs3org/reva/pkg/user"
"github.com/micro/go-micro/v2/metadata"
"github.com/owncloud/ocis/ocis-pkg/account"
)
@@ -49,18 +50,21 @@ func ExtractAccountUUID(opts ...account.Option) func(http.Handler) http.Handler
return
}
user, err := tokenManager.DismantleToken(r.Context(), token)
u, err := tokenManager.DismantleToken(r.Context(), token)
if err != nil {
opt.Logger.Error().Err(err)
return
}
// store user in context for request
ctx := user.ContextSetUser(r.Context(), u)
// Important: user.Id.OpaqueId is the AccountUUID. Set this way in the account uuid middleware in ocis-proxy.
// https://github.com/owncloud/ocis-proxy/blob/ea254d6036592cf9469d757d1295e0c4309d1e63/pkg/middleware/account_uuid.go#L109
ctx := context.WithValue(r.Context(), UUIDKey, user.Id.OpaqueId)
ctx = context.WithValue(ctx, UUIDKey, u.Id.OpaqueId)
// TODO: implement token manager in cs3org/reva that uses generic metadata instead of access token from header.
ctx = metadata.Set(ctx, AccountID, user.Id.OpaqueId)
ctx = metadata.Set(ctx, RoleIDs, string(user.Opaque.Map["roles"].Value))
ctx = metadata.Set(ctx, AccountID, u.Id.OpaqueId)
ctx = metadata.Set(ctx, RoleIDs, string(u.Opaque.Map["roles"].Value))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
@@ -202,18 +202,12 @@ apiProvisioning-v2/apiProvisioningUsingAppPassword.feature:67
# https://github.com/owncloud/ocis-ocs/issues/28
# disable users /cloud/users/disable|enable not available
#
apiProvisioning-v1/disableUser.feature:11
apiProvisioning-v1/disableUser.feature:79
apiProvisioning-v1/disableUser.feature:99
apiProvisioning-v1/disableUser.feature:107
apiProvisioning-v1/disableUser.feature:129
apiProvisioning-v1/enableUser.feature:11
apiProvisioning-v2/disableUser.feature:11
apiProvisioning-v2/disableUser.feature:79
apiProvisioning-v2/disableUser.feature:99
apiProvisioning-v2/disableUser.feature:108
apiProvisioning-v2/disableUser.feature:130
apiProvisioning-v2/enableUser.feature:11
#
# https://github.com/owncloud/ocis-ocs/issues/51
# displayname of user can be changed to empty
@@ -226,9 +220,6 @@ apiProvisioning-v2/editUser.feature:47
#
apiProvisioning-v1/editUser.feature:56
apiProvisioning-v1/editUser.feature:122
apiProvisioning-v1/enableUser.feature:34
apiProvisioning-v1/enableUser.feature:56
apiProvisioning-v1/enableUser.feature:63
apiProvisioning-v2/editUser.feature:56
apiProvisioning-v2/editUser.feature:122
apiProvisioning-v2/enableUser.feature:34
@@ -238,14 +229,10 @@ apiProvisioning-v2/enableUser.feature:64
# https://github.com/owncloud/product/issues/248
# user can get info of other users/ cloud/users endpoints not authenticated
#
apiProvisioning-v1/deleteUser.feature:53
apiProvisioning-v2/deleteUser.feature:54
apiProvisioning-v1/getUser.feature:81
apiProvisioning-v1/getUsers.feature:43
apiProvisioning-v1/resetUserPassword.feature:56
apiProvisioning-v2/getUser.feature:82
apiProvisioning-v2/getUsers.feature:44
apiProvisioning-v2/resetUserPassword.feature:56
#
# https://github.com/owncloud/product/issues/250
# incorrect ocs(v2) status value when getting info of user that does not exist should be 404, gives 998
@@ -315,7 +302,6 @@ apiSharees/sharees.feature:538
# https://github.com/owncloud/ocis-reva/issues/34 groups endpoint does not exist
#
apiShareManagementToShares/acceptShares.feature:22
apiShareManagementToShares/acceptShares.feature:52
apiShareManagementToShares/acceptShares.feature:71
apiShareManagementToShares/acceptShares.feature:156
apiShareManagementToShares/acceptShares.feature:157
@@ -331,12 +317,9 @@ apiShareManagementToShares/acceptShares.feature:249
apiShareManagementToShares/acceptShares.feature:270
apiShareManagementToShares/acceptShares.feature:279
apiShareManagementToShares/acceptShares.feature:298
apiShareManagementToShares/acceptShares.feature:320
apiShareManagementToShares/acceptShares.feature:342
apiShareManagementToShares/acceptShares.feature:378
apiShareManagementToShares/acceptShares.feature:398
apiShareManagementToShares/acceptShares.feature:417
apiShareManagementToShares/acceptShares.feature:439
#
# https://github.com/owncloud/product/issues/207 Response is empty when accepting a share
#
@@ -401,12 +384,8 @@ apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.fe
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:70
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:97
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:98
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:115
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:116
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:135
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:136
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:153
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:154
#
# https://github.com/owncloud/ocis/issues/720 shares are mounted into /Shares folder even after the sharer deletes the collaborator
# https://github.com/owncloud/ocis/issues/721 deleting share response does not contain `data` field
@@ -451,11 +430,6 @@ apiShareOperationsToShares/accessToShare.feature:56
apiShareOperationsToShares/gettingShares.feature:24
apiShareOperationsToShares/gettingShares.feature:25
#
# https://github.com/owncloud/ocis-reva/issues/65 There is no such thing like a "super-user"
#
apiShareOperationsToShares/gettingShares.feature:38
apiShareOperationsToShares/gettingShares.feature:39
#
# https://github.com/owncloud/ocis-reva/issues/357 Delete shares from user when user is deleted
# https://github.com/owncloud/ocis-reva/issues/301 no displayname_owner shown when creating a share
# https://github.com/owncloud/ocis-reva/issues/302 when sharing a file mime-type field is set to application/octet-stream
@@ -549,11 +523,8 @@ apiSharePublicLink1/changingPublicLinkShare.feature:96
#
apiSharePublicLink1/changingPublicLinkShare.feature:63
apiSharePublicLink1/changingPublicLinkShare.feature:107
apiSharePublicLink1/changingPublicLinkShare.feature:128
apiSharePublicLink1/changingPublicLinkShare.feature:151
apiSharePublicLink1/changingPublicLinkShare.feature:174
apiSharePublicLink1/changingPublicLinkShare.feature:197
apiSharePublicLink1/changingPublicLinkShare.feature:221
apiSharePublicLink1/changingPublicLinkShare.feature:244
#
# https://github.com/owncloud/ocis-reva/issues/292 Public link enforce permissions
@@ -708,6 +679,11 @@ apiSharePublicLink2/uploadToPublicLinkShare.feature:103
apiSharePublicLink2/uploadToPublicLinkShare.feature:121
apiSharePublicLink2/uploadToPublicLinkShare.feature:139
#
# https://github.com/owncloud/ocis/issues/801 deleting a folder should delete share links to it as well
#
apiSharePublicLink2/uploadToPublicLinkShare.feature:48
apiSharePublicLink2/uploadToPublicLinkShare.feature:49
#
# https://github.com/owncloud/ocis-reva/issues/286 Upload-only shares must not overwrite but create a separate file
#
apiSharePublicLink2/uploadToPublicLinkShare.feature:23
@@ -1190,8 +1166,6 @@ apiWebdavOperations/downloadFile.feature:84
apiWebdavOperations/downloadFile.feature:85
apiWebdavOperations/refuseAccess.feature:21
apiWebdavOperations/refuseAccess.feature:22
apiWebdavOperations/refuseAccess.feature:33
apiWebdavOperations/refuseAccess.feature:34
#
# https://github.com/owncloud/core/pull/38035 PROPFIND to https://localhost:9200/remote.php/dav/files gets an error 500 response
#
@@ -1535,15 +1509,6 @@ apiWebdavPreviews/previews.feature:166
apiWebdavPreviews/previews.feature:178
apiWebdavPreviews/previews.feature:179
#
# https://github.com/owncloud/ocis-ocs/issues/35 group support is not yet implemented
#
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:93
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:94
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:114
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:115
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:116
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:117
#
# https://github.com/owncloud/product/issues/241 deleting an item updates etags of grandparent but not on parent
#
apiWebdavEtagPropagation1/deleteFileFolder.feature:25
@@ -172,24 +172,12 @@ apiProvisioning-v2/enableUser.feature:32
apiProvisioning-v2/getUser.feature:34
apiProvisioning-v2/getUser.feature:35
#
# https://github.com/owncloud/ocis-accounts/issues/80
# Creating an already existing user works
#
apiProvisioning-v1/addUser.feature:32
apiProvisioning-v1/addUser.feature:39
apiProvisioning-v2/addUser.feature:39
#
# https://github.com/owncloud/product/issues/197
# Password can be set to empty
#
apiProvisioning-v1/addUser.feature:69
apiProvisioning-v2/addUser.feature:69
#
# https://github.com/owncloud/ocis-accounts/issues/128
# Username is case sensitive
#
apiProvisioning-v1/addUser.feature:102
#
# https://github.com/owncloud/ocis/issues/197
# Client token generation not implemented
#
@@ -201,18 +189,12 @@ apiProvisioning-v2/apiProvisioningUsingAppPassword.feature:67
# https://github.com/owncloud/ocis-ocs/issues/28
# disable users /cloud/users/disable|enable not available
#
apiProvisioning-v1/disableUser.feature:11
apiProvisioning-v1/disableUser.feature:79
apiProvisioning-v1/disableUser.feature:99
apiProvisioning-v1/disableUser.feature:107
apiProvisioning-v1/disableUser.feature:129
apiProvisioning-v1/enableUser.feature:11
apiProvisioning-v2/disableUser.feature:11
apiProvisioning-v2/disableUser.feature:79
apiProvisioning-v2/disableUser.feature:99
apiProvisioning-v2/disableUser.feature:108
apiProvisioning-v2/disableUser.feature:130
apiProvisioning-v2/enableUser.feature:11
#
# https://github.com/owncloud/ocis-ocs/issues/51
# displayname of user can be changed to empty
@@ -225,9 +207,6 @@ apiProvisioning-v2/editUser.feature:47
#
apiProvisioning-v1/editUser.feature:56
apiProvisioning-v1/editUser.feature:122
apiProvisioning-v1/enableUser.feature:34
apiProvisioning-v1/enableUser.feature:56
apiProvisioning-v1/enableUser.feature:63
apiProvisioning-v2/editUser.feature:56
apiProvisioning-v2/editUser.feature:122
apiProvisioning-v2/enableUser.feature:34
@@ -237,14 +216,10 @@ apiProvisioning-v2/enableUser.feature:64
# https://github.com/owncloud/product/issues/248
# user can get info of other users/ cloud/users endpoints not authenticated
#
apiProvisioning-v1/deleteUser.feature:53
apiProvisioning-v2/deleteUser.feature:54
apiProvisioning-v1/getUser.feature:81
apiProvisioning-v1/getUsers.feature:43
apiProvisioning-v1/resetUserPassword.feature:56
apiProvisioning-v2/getUser.feature:82
apiProvisioning-v2/getUsers.feature:44
apiProvisioning-v2/resetUserPassword.feature:56
#
# https://github.com/owncloud/product/issues/250
# incorrect ocs(v2) status value when getting info of user that does not exist should be 404, gives 998
@@ -314,7 +289,6 @@ apiSharees/sharees.feature:538
# https://github.com/owncloud/ocis-reva/issues/34 groups endpoint does not exist
#
apiShareManagementToShares/acceptShares.feature:22
apiShareManagementToShares/acceptShares.feature:52
apiShareManagementToShares/acceptShares.feature:71
apiShareManagementToShares/acceptShares.feature:156
apiShareManagementToShares/acceptShares.feature:157
@@ -330,12 +304,9 @@ apiShareManagementToShares/acceptShares.feature:249
apiShareManagementToShares/acceptShares.feature:270
apiShareManagementToShares/acceptShares.feature:279
apiShareManagementToShares/acceptShares.feature:298
apiShareManagementToShares/acceptShares.feature:320
apiShareManagementToShares/acceptShares.feature:342
apiShareManagementToShares/acceptShares.feature:378
apiShareManagementToShares/acceptShares.feature:398
apiShareManagementToShares/acceptShares.feature:417
apiShareManagementToShares/acceptShares.feature:439
#
# https://github.com/owncloud/product/issues/207 Response is empty when accepting a share
#
@@ -400,12 +371,8 @@ apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.fe
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:70
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:97
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:98
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:115
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:116
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:135
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:136
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:153
apiShareManagementBasicToShares/excludeGroupFromReceivingSharesToSharesFolder.feature:154
#
# https://github.com/owncloud/ocis/issues/720 shares are mounted into /Shares folder even after the sharer deletes the collaborator
# https://github.com/owncloud/ocis/issues/721 deleting share response does not contain `data` field
@@ -443,11 +410,6 @@ apiShareOperationsToShares/accessToShare.feature:56
apiShareOperationsToShares/gettingShares.feature:24
apiShareOperationsToShares/gettingShares.feature:25
#
# https://github.com/owncloud/ocis-reva/issues/65 There is no such thing like a "super-user"
#
apiShareOperationsToShares/gettingShares.feature:38
apiShareOperationsToShares/gettingShares.feature:39
#
# https://github.com/owncloud/ocis-reva/issues/357 Delete shares from user when user is deleted
# https://github.com/owncloud/ocis-reva/issues/301 no displayname_owner shown when creating a share
# https://github.com/owncloud/ocis-reva/issues/302 when sharing a file mime-type field is set to application/octet-stream
@@ -526,11 +488,8 @@ apiSharePublicLink1/changingPublicLinkShare.feature:96
#
apiSharePublicLink1/changingPublicLinkShare.feature:63
apiSharePublicLink1/changingPublicLinkShare.feature:107
apiSharePublicLink1/changingPublicLinkShare.feature:128
apiSharePublicLink1/changingPublicLinkShare.feature:151
apiSharePublicLink1/changingPublicLinkShare.feature:174
apiSharePublicLink1/changingPublicLinkShare.feature:197
apiSharePublicLink1/changingPublicLinkShare.feature:221
apiSharePublicLink1/changingPublicLinkShare.feature:244
#
# https://github.com/owncloud/ocis-reva/issues/292 Public link enforce permissions
@@ -690,6 +649,11 @@ apiSharePublicLink2/uploadToPublicLinkShare.feature:103
apiSharePublicLink2/uploadToPublicLinkShare.feature:121
apiSharePublicLink2/uploadToPublicLinkShare.feature:139
#
# https://github.com/owncloud/ocis/issues/801 deleting a folder should delete share links to it as well
#
apiSharePublicLink2/uploadToPublicLinkShare.feature:48
apiSharePublicLink2/uploadToPublicLinkShare.feature:49
#
# https://github.com/owncloud/ocis-reva/issues/286 Upload-only shares must not overwrite but create a separate file
#
apiSharePublicLink2/uploadToPublicLinkShare.feature:23
@@ -1175,8 +1139,6 @@ apiWebdavOperations/downloadFile.feature:84
apiWebdavOperations/downloadFile.feature:85
apiWebdavOperations/refuseAccess.feature:21
apiWebdavOperations/refuseAccess.feature:22
apiWebdavOperations/refuseAccess.feature:33
apiWebdavOperations/refuseAccess.feature:34
#
# https://github.com/owncloud/core/pull/38035 PROPFIND to https://localhost:9200/remote.php/dav/files gets an error 500 response
#
@@ -1492,15 +1454,6 @@ apiWebdavPreviews/previews.feature:166
apiWebdavPreviews/previews.feature:178
apiWebdavPreviews/previews.feature:179
#
# https://github.com/owncloud/ocis-ocs/issues/35 group support is not yet implemented
#
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:93
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:94
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:114
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:115
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:116
apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:117
#
# https://github.com/owncloud/ocis/issues/762 path and other information are not shown if a share does not have "read" permission
#
apiShareOperationsToShares/uploadToShare.feature:64
@@ -3,6 +3,7 @@
Feature: auth
# these endpoints are handled by the reva ocs implementation
Scenario: send DELETE requests to OCS endpoints as admin with wrong password
When the administrator requests these endpoints with "DELETE" using password "invalid" about user "Alice"
| endpoint |
@@ -20,39 +21,15 @@ Feature: auth
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "notset"
# these endpoints are handled by the ocis ocs implementation
Scenario: send DELETE requests to OCS endpoints as admin with wrong password
When the administrator requests these endpoints with "DELETE" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v1.php/cloud/users/%username% |
| /ocs/v1.php/cloud/users/%username%/subadmins |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "998"
Scenario: send DELETE requests to OCS endpoints as admin with wrong password
When the administrator requests these endpoints with "DELETE" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v2.php/cloud/users/%username% |
Then the HTTP status code of responses on all endpoints should be "404"
And the OCS status code of responses on all endpoints should be "998"
Scenario: send DELETE requests to OCS endpoints as admin with wrong password
When the administrator requests these endpoints with "DELETE" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v1.php/cloud/users/%username%/groups |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "996"
Scenario: send DELETE requests to OCS endpoints as admin with wrong password
When the administrator requests these endpoints with "DELETE" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v2.php/cloud/users/%username%/groups |
Then the HTTP status code of responses on all endpoints should be "500"
And the OCS status code of responses on all endpoints should be "996"
Scenario: send DELETE requests to OCS endpoints as admin with wrong password
When the administrator requests these endpoints with "DELETE" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v2.php/cloud/users/%username% |
| /ocs/v1.php/cloud/users/%username%/subadmins |
| /ocs/v2.php/cloud/users/%username%/subadmins |
Then the HTTP status code of responses on all endpoints should be "404"
And the OCS status code of responses on all endpoints should be "998"
| /ocs/v1.php/cloud/users/%username%/groups |
| /ocs/v2.php/cloud/users/%username%/groups |
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "notset"
@@ -20,8 +20,6 @@ Feature: auth
| /ocs/v2.php/apps/files_sharing/api/v1/shares |
| /ocs/v1.php/cloud/apps |
| /ocs/v2.php/cloud/apps |
| /ocs/v1.php/cloud/groups |
| /ocs/v2.php/cloud/groups |
| /ocs/v1.php/config |
| /ocs/v2.php/config |
| /ocs/v1.php/privatedata/getattribute |
@@ -33,15 +31,13 @@ Feature: auth
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario: using OCS anonymously
When a user requests these endpoints with "GET" and no authentication
| endpoint |
| /ocs/v1.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "100"
When a user requests these endpoints with "GET" and no authentication
| endpoint |
| /ocs/v2.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "200"
| endpoint |
| /ocs/v1.php/cloud/users |
| /ocs/v2.php/cloud/users |
| /ocs/v1.php/cloud/groups |
| /ocs/v2.php/cloud/groups |
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "997"
@issue-ocis-reva-11
@@ -59,7 +55,6 @@ Feature: auth
| /ocs/v1.php/apps/files_sharing/api/v1/remote_shares |
| /ocs/v1.php/apps/files_sharing/api/v1/remote_shares/pending |
| /ocs/v1.php/privatedata/getattribute |
| /ocs/v1.php/cloud/groups |
| /ocs/v1.php/cloud/apps |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "998"
@@ -77,20 +72,17 @@ Feature: auth
# | /ocs/v2.php/apps/files_sharing/api/v1/shares | 100 | 200 |
| /ocs/v2.php/cloud/apps |
| /ocs/v2.php/cloud/groups |
| /ocs/v2.php/privatedata/getattribute |
Then the HTTP status code of responses on all endpoints should be "404"
And the OCS status code of responses on all endpoints should be "998"
When the user "Alice" requests these endpoints with "GET" with basic auth
| endpoint |
| /ocs/v1.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "100"
When the user "Alice" requests these endpoints with "GET" with basic auth
| endpoint |
| /ocs/v2.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "200"
| endpoint |
| /ocs/v1.php/cloud/users |
| /ocs/v2.php/cloud/users |
| /ocs/v1.php/cloud/groups |
| /ocs/v2.php/cloud/groups |
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "997"
When the user "Alice" requests these endpoints with "GET" with basic auth
| endpoint |
| /ocs/v2.php/config |
@@ -133,13 +125,9 @@ Feature: auth
When user "Alice" requests these endpoints with "GET" using password "invalid"
| endpoint |
| /ocs/v1.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "100"
When user "Alice" requests these endpoints with "GET" using password "invalid"
| endpoint |
| /ocs/v2.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "200"
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "notset"
@skipOnOcV10
@issue-ocis-reva-29
@@ -183,10 +171,6 @@ Feature: auth
When user "brian" requests these endpoints with "GET" using password "invalid"
| endpoint |
| /ocs/v1.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "100"
When user "brian" requests these endpoints with "GET" using password "invalid"
| endpoint |
| /ocs/v2.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "200"
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "notset"
@@ -28,43 +28,16 @@ Feature: auth
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "notset"
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario: send POST requests to OCS endpoints as normal user with wrong password
When user "Alice" requests these endpoints with "POST" including body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v1.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "400"
When user "Alice" requests these endpoints with "POST" including body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v2.php/cloud/users |
Then the HTTP status code of responses on all endpoints should be "400"
And the OCS status code of responses on all endpoints should be "400"
@issue-ocis-reva-30
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario: send POST requests to OCS endpoints as normal user with wrong password
When user "Alice" requests these endpoints with "POST" including body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v1.php/cloud/users/%username%/groups |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "400"
When user "Alice" requests these endpoints with "POST" including body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v2.php/cloud/users/%username%/groups |
Then the HTTP status code of responses on all endpoints should be "400"
And the OCS status code of responses on all endpoints should be "400"
@issue-ocis-reva-30
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario: send POST requests to OCS endpoints as normal user with wrong password
When user "Alice" requests these endpoints with "POST" including body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v1.php/cloud/users |
| /ocs/v2.php/cloud/users |
| /ocs/v1.php/cloud/users/%username%/groups |
| /ocs/v2.php/cloud/users/%username%/groups |
| /ocs/v1.php/cloud/users/%username%/subadmins |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "998"
When user "Alice" requests these endpoints with "POST" including body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v2.php/cloud/users/%username%/subadmins |
Then the HTTP status code of responses on all endpoints should be "404"
And the OCS status code of responses on all endpoints should be "998"
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "notset"
@@ -9,24 +9,11 @@ Feature: auth
| endpoint |
| /ocs/v1.php/apps/files_sharing/api/v1/shares/123 |
| /ocs/v2.php/apps/files_sharing/api/v1/shares/123 |
| /ocs/v1.php/cloud/users/%username% |
| /ocs/v2.php/cloud/users/%username% |
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "notset"
@issue-ocis-reva-30
@issue-ocis-ocs-26
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario: send PUT request to OCS endpoints as admin with wrong password
When the administrator requests these endpoints with "PUT" with body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v1.php/cloud/users/%username% |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "998"
When the administrator requests these endpoints with "PUT" with body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v2.php/cloud/users/%username% |
Then the HTTP status code of responses on all endpoints should be "404"
And the OCS status code of responses on all endpoints should be "998"
@issue-ocis-reva-30
@issue-ocis-ocs-28
# after fixing all issues delete this Scenario and use the one from oC10 core
@@ -34,13 +21,9 @@ Feature: auth
When the administrator requests these endpoints with "PUT" with body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v1.php/cloud/users/%username%/disable |
| /ocs/v1.php/cloud/users/%username%/enable |
Then the HTTP status code of responses on all endpoints should be "200"
And the OCS status code of responses on all endpoints should be "998"
When the administrator requests these endpoints with "PUT" with body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /ocs/v2.php/cloud/users/%username%/disable |
| /ocs/v1.php/cloud/users/%username%/enable |
| /ocs/v2.php/cloud/users/%username%/enable |
Then the HTTP status code of responses on all endpoints should be "404"
And the OCS status code of responses on all endpoints should be "998"
Then the HTTP status code of responses on all endpoints should be "401"
And the OCS status code of responses on all endpoints should be "notset"
@@ -15,3 +15,19 @@ Feature: upload to a public link share
When user "Alice" deletes file "/FOLDER" using the WebDAV API
And the public uploads file "does-not-matter.txt" with content "does not matter" using the new public WebDAV API
Then the HTTP status code should be "500"
@issue-ocis-801
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario Outline: Uploading file to a public upload-only share using old public API that was deleted does not work
Given using <dav-path> DAV path
And user "Alice" has created a public link share with settings
| path | FOLDER |
| permissions | create |
When user "Alice" deletes file "/FOLDER" using the WebDAV API
Then uploading a file should not work using the old public WebDAV API
And the HTTP status code should be "401"
Examples:
| dav-path |
| old |
| new |
+2
View File
@@ -7,6 +7,7 @@ require (
contrib.go.opencensus.io/exporter/ocagent v0.7.0
contrib.go.opencensus.io/exporter/zipkin v0.1.1
github.com/UnnoTed/fileb0x v1.1.4
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00
github.com/cs3org/reva v1.3.1-0.20201023144216-cdb3d6688da5
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/render v1.0.1
@@ -26,6 +27,7 @@ require (
github.com/stretchr/testify v1.6.1
go.opencensus.io v0.22.5
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
google.golang.org/genproto v0.0.0-20200624020401-64a14ca9d1ad
google.golang.org/protobuf v1.25.0
)
+39 -1061
View File
File diff suppressed because it is too large Load Diff
-40
View File
@@ -1,40 +0,0 @@
package middleware
import (
"net/http"
"github.com/cs3org/reva/pkg/token/manager/jwt"
"github.com/cs3org/reva/pkg/user"
)
// AccessToken middleware is used to set the user from an x-access-token to the context
func AccessToken(opts ...Option) func(next http.Handler) http.Handler {
opt := newOptions(opts...)
return func(next http.Handler) http.Handler {
// TODO: handle error
tokenManager, err := jwt.New(map[string]interface{}{
"secret": opt.TokenManagerConfig.JWTSecret,
"expires": int64(60),
})
if err != nil {
opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager")
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("x-access-token")
if token != "" {
u, err := tokenManager.DismantleToken(r.Context(), token)
if err != nil {
opt.Logger.Error().Err(err).Msg("could not dismantle token")
w.WriteHeader(http.StatusInternalServerError)
return
}
// store user in context for request
r = r.WithContext(user.ContextSetUser(r.Context(), u))
}
next.ServeHTTP(w, r)
})
}
}
+6 -6
View File
@@ -1,8 +1,8 @@
package middleware
import (
"github.com/owncloud/ocis/ocs/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/roles"
)
// Option defines a single option function.
@@ -12,8 +12,8 @@ type Option func(o *Options)
type Options struct {
// Logger to use for logging, must be set
Logger log.Logger
// TokenManagerConfig for communicating with the reva token manager
TokenManagerConfig config.TokenManager
// RoleManager for looking up permissions
RoleManager *roles.Manager
}
// newOptions initializes the available default options.
@@ -34,9 +34,9 @@ func Logger(l log.Logger) Option {
}
}
// TokenManagerConfig provides a function to set the token manger config option.
func TokenManagerConfig(cfg config.TokenManager) Option {
// RoleManager provides a function to set the RoleManager option.
func RoleManager(val *roles.Manager) Option {
return func(o *Options) {
o.TokenManagerConfig = cfg
o.RoleManager = val
}
}
+37
View File
@@ -0,0 +1,37 @@
package middleware
import (
"net/http"
"github.com/go-chi/render"
accounts "github.com/owncloud/ocis/accounts/pkg/service/v0"
"github.com/owncloud/ocis/ocis-pkg/roles"
"github.com/owncloud/ocis/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/ocs/pkg/service/v0/response"
)
// RequireAdmin middleware is used to require the user in context to be an admin / have account management permissions
func RequireAdmin(opts ...Option) func(next http.Handler) http.Handler {
opt := newOptions(opts...)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// get roles from context
roleIDs, ok := roles.ReadRoleIDsFromContext(r.Context())
if !ok {
render.Render(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
return
}
// check if permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.AccountManagementPermissionID) != nil {
next.ServeHTTP(w, r)
return
}
render.Render(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
})
}
}
+57
View File
@@ -0,0 +1,57 @@
package middleware
import (
"net/http"
"github.com/cs3org/reva/pkg/user"
"github.com/go-chi/chi"
"github.com/go-chi/render"
accounts "github.com/owncloud/ocis/accounts/pkg/service/v0"
"github.com/owncloud/ocis/ocis-pkg/roles"
"github.com/owncloud/ocis/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/ocs/pkg/service/v0/response"
)
// RequireSelfOrAdmin middleware is used to require the requesting user to be an admin or the requested user himself
func RequireSelfOrAdmin(opts ...Option) func(next http.Handler) http.Handler {
opt := newOptions(opts...)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, ok := user.ContextGetUser(r.Context())
if !ok {
render.Render(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
return
}
if u.Id == nil || u.Id.OpaqueId == "" {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "user is missing an id"))
return
}
// get roles from context
roleIDs, ok := roles.ReadRoleIDsFromContext(r.Context())
if !ok {
render.Render(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
return
}
// check if account management permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.AccountManagementPermissionID) != nil {
next.ServeHTTP(w, r)
return
}
// check if self management permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.SelfManagementPermissionID) != nil {
userid := chi.URLParam(r, "userid")
if userid == "" || userid == u.Id.OpaqueId || userid == u.Username {
next.ServeHTTP(w, r)
return
}
}
render.Render(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
})
}
}
+32
View File
@@ -0,0 +1,32 @@
package middleware
import (
"net/http"
"github.com/cs3org/reva/pkg/user"
"github.com/go-chi/render"
"github.com/owncloud/ocis/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/ocs/pkg/service/v0/response"
)
// RequireUser middleware is used to require a user in context
func RequireUser() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, ok := user.ContextGetUser(r.Context())
if !ok {
render.Render(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
return
}
if u.Id == nil || u.Id.OpaqueId == "" {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "user is missing an id"))
return
}
next.ServeHTTP(w, r)
})
}
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -18,6 +18,9 @@ var MetaFailure = Meta{Status: "", StatusCode: 101, Message: "Failure"}
// MetaInvalidInput is an error response with code 102
var MetaInvalidInput = Meta{Status: "", StatusCode: 102, Message: "Invalid Input"}
// MetaForbidden is an error response with code 104
var MetaForbidden = Meta{Status: "", StatusCode: 104, Message: "Forbidden"}
// MetaBadRequest is used for unknown errors
var MetaBadRequest = Meta{Status: "error", StatusCode: 400, Message: "Bad Request"}
+179 -19
View File
@@ -1,9 +1,13 @@
package svc
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/render"
@@ -41,10 +45,28 @@ func (o Ocs) ListUserGroups(w http.ResponseWriter, r *http.Request) {
groups := []string{}
for i := range account.MemberOf {
groups = append(groups, account.MemberOf[i].Id)
if account.MemberOf[i].OnPremisesSamAccountName == "" {
o.logger.Warn().Str("groupid", account.MemberOf[i].Id).Msg("group on_premises_sam_account_name is empty, trying to lookup by id")
// we can try to look up the name
group, err := o.getGroupsService().GetGroup(r.Context(), &accounts.GetGroupRequest{
Id: account.MemberOf[i].Id,
})
if err != nil {
o.logger.Error().Err(err).Str("groupid", account.MemberOf[i].Id).Msg("could not get group")
continue
}
if group.OnPremisesSamAccountName == "" {
o.logger.Error().Err(err).Str("groupid", account.MemberOf[i].Id).Msg("group on_premises_sam_account_name is empty")
continue
}
groups = append(groups, group.OnPremisesSamAccountName)
} else {
groups = append(groups, account.MemberOf[i].OnPremisesSamAccountName)
}
}
o.logger.Error().Err(err).Int("count", len(groups)).Str("userid", userid).Msg("listing groups for user")
o.logger.Error().Err(err).Int("count", len(groups)).Str("userid", account.Id).Msg("listing groups for user")
render.Render(w, r, response.DataRender(&data.Groups{Groups: groups}))
}
@@ -69,9 +91,21 @@ func (o Ocs) AddToGroup(w http.ResponseWriter, r *http.Request) {
return
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested group could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().AddMember(r.Context(), &accounts.AddMemberRequest{
AccountId: account.Id,
GroupId: groupid,
GroupId: group.Id,
})
if err != nil {
@@ -81,21 +115,46 @@ func (o Ocs) AddToGroup(w http.ResponseWriter, r *http.Request) {
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Str("groupid", groupid).Msg("could not add user to group")
o.logger.Error().Err(err).Str("userid", account.Id).Str("groupid", group.Id).Msg("could not add user to group")
return
}
o.logger.Debug().Str("userid", userid).Str("groupid", groupid).Msg("added user to group")
o.logger.Debug().Str("userid", account.Id).Str("groupid", group.Id).Msg("added user to group")
render.Render(w, r, response.DataRender(struct{}{}))
}
// RemoveFromGroup removes a user from a group
func (o Ocs) RemoveFromGroup(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
groupid := r.URL.Query().Get("groupid")
var err error
// Really? a DELETE with form encoded body?!?
// but it is not encoded as mime, so we cannot just call r.ParseForm()
// read it manually
body, err := ioutil.ReadAll(r.Body)
if err != nil {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
return
}
if err = r.Body.Close(); err != nil {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
return
}
values, err := url.ParseQuery(string(body))
if err != nil {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
return
}
groupid := values.Get("groupid")
if groupid == "" {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
return
}
var account *accounts.Account
var err error
if isValidUUID(userid) {
account, _ = o.getAccountService().GetAccount(r.Context(), &accounts.GetAccountRequest{
@@ -116,9 +175,21 @@ func (o Ocs) RemoveFromGroup(w http.ResponseWriter, r *http.Request) {
}
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested group could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().RemoveMember(r.Context(), &accounts.RemoveMemberRequest{
AccountId: account.Id,
GroupId: groupid,
GroupId: group.Id,
})
if err != nil {
@@ -128,11 +199,11 @@ func (o Ocs) RemoveFromGroup(w http.ResponseWriter, r *http.Request) {
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Str("groupid", groupid).Msg("could not remove user from group")
o.logger.Error().Err(err).Str("userid", account.Id).Str("groupid", group.Id).Msg("could not remove user from group")
return
}
o.logger.Debug().Str("userid", userid).Str("groupid", groupid).Msg("removed user from group")
o.logger.Debug().Str("userid", account.Id).Str("groupid", group.Id).Msg("removed user from group")
render.Render(w, r, response.DataRender(struct{}{}))
}
@@ -156,7 +227,7 @@ func (o Ocs) ListGroups(w http.ResponseWriter, r *http.Request) {
groups := []string{}
for i := range res.Groups {
groups = append(groups, res.Groups[i].Id)
groups = append(groups, res.Groups[i].OnPremisesSamAccountName)
}
render.Render(w, r, response.DataRender(&data.Groups{Groups: groups}))
@@ -164,15 +235,78 @@ func (o Ocs) ListGroups(w http.ResponseWriter, r *http.Request) {
// AddGroup adds a group
func (o Ocs) AddGroup(w http.ResponseWriter, r *http.Request) {
render.Render(w, r, response.ErrRender(data.MetaUnknownError.StatusCode, "not implemented"))
groupid := r.PostFormValue("groupid")
displayname := r.PostFormValue("displayname")
gid := r.PostFormValue("gidnumber")
var gidNumber int64
var err error
if gid != "" {
gidNumber, err = strconv.ParseInt(gid, 10, 64)
if err != nil {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "Cannot use the gidnumber provided"))
o.logger.Error().Err(err).Str("gid", gid).Str("groupid", groupid).Msg("Cannot use the gidnumber provided")
return
}
}
if displayname == "" {
displayname = groupid
}
newGroup := &accounts.Group{
Id: groupid,
DisplayName: displayname,
OnPremisesSamAccountName: groupid,
GidNumber: gidNumber,
}
group, err := o.getGroupsService().CreateGroup(r.Context(), &accounts.CreateGroupRequest{
Group: newGroup,
})
if err != nil {
merr := merrors.FromError(err)
switch merr.Code {
case http.StatusBadRequest:
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
case http.StatusConflict:
if response.APIVersion(r.Context()) == "2" {
// it seems the application framework sets the ocs status code to the httpstatus code, which affects the provisioning api
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
render.Render(w, r, response.ErrRender(data.MetaInvalidInput.StatusCode, merr.Detail))
}
default:
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", groupid).Msg("could not add group")
// TODO check error if group already existed
return
}
o.logger.Debug().Interface("group", group).Msg("added group")
render.Render(w, r, response.DataRender(struct{}{}))
}
// DeleteGroup deletes a group
func (o Ocs) DeleteGroup(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
_, err := o.getGroupsService().DeleteGroup(r.Context(), &accounts.DeleteGroupRequest{
Id: groupid,
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested group could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().DeleteGroup(r.Context(), &accounts.DeleteGroupRequest{
Id: group.Id,
})
if err != nil {
@@ -182,11 +316,11 @@ func (o Ocs) DeleteGroup(w http.ResponseWriter, r *http.Request) {
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", groupid).Msg("could not remove group")
o.logger.Error().Err(err).Str("groupid", group.Id).Msg("could not remove group")
return
}
o.logger.Debug().Str("groupid", groupid).Msg("removed group")
o.logger.Debug().Str("groupid", group.Id).Msg("removed group")
render.Render(w, r, response.DataRender(struct{}{}))
}
@@ -195,7 +329,19 @@ func (o Ocs) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
res, err := o.getGroupsService().ListMembers(r.Context(), &accounts.ListMembersRequest{Id: groupid})
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested group could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
res, err := o.getGroupsService().ListMembers(r.Context(), &accounts.ListMembersRequest{Id: group.Id})
if err != nil {
merr := merrors.FromError(err)
@@ -204,13 +350,13 @@ func (o Ocs) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", groupid).Msg("could not get list of members")
o.logger.Error().Err(err).Str("groupid", group.Id).Msg("could not get list of members")
return
}
members := []string{}
for i := range res.Members {
members = append(members, res.Members[i].Id)
members = append(members, res.Members[i].OnPremisesSamAccountName)
}
o.logger.Error().Err(err).Int("count", len(members)).Str("groupid", groupid).Msg("listing group members")
@@ -221,3 +367,17 @@ func isValidUUID(uuid string) bool {
r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
return r.MatchString(uuid)
}
func (o Ocs) fetchGroupByName(ctx context.Context, name string) (*accounts.Group, error) {
var res *accounts.ListGroupsResponse
res, err := o.getGroupsService().ListGroups(ctx, &accounts.ListGroupsRequest{
Query: fmt.Sprintf("on_premises_sam_account_name eq '%v'", escapeValue(name)),
})
if err != nil {
return nil, err
}
if res != nil && len(res.Groups) == 1 {
return res.Groups[0], nil
}
return nil, merrors.NotFound("", "The requested group could not be found")
}
+22 -4
View File
@@ -3,8 +3,10 @@ package svc
import (
"net/http"
"github.com/owncloud/ocis/ocs/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/roles"
"github.com/owncloud/ocis/ocs/pkg/config"
settings "github.com/owncloud/ocis/settings/pkg/proto/v0"
)
// Option defines a single option function.
@@ -12,9 +14,11 @@ type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
RoleService settings.RoleService
RoleManager *roles.Manager
}
// newOptions initializes the available default options.
@@ -48,3 +52,17 @@ func Middleware(val ...func(http.Handler) http.Handler) Option {
o.Middleware = val
}
}
// RoleService provides a function to set the RoleService option.
func RoleService(val settings.RoleService) Option {
return func(o *Options) {
o.RoleService = val
}
}
// RoleManager provides a function to set the RoleManager option.
func RoleManager(val *roles.Manager) Option {
return func(o *Options) {
o.RoleManager = val
}
}
+10 -2
View File
@@ -32,10 +32,17 @@ func APIVersion(ctx context.Context) string {
// OcsV1StatusCodes returns the http status codes for the OCS API v1.
func OcsV1StatusCodes(meta data.Meta) int {
if meta.StatusCode == data.MetaUnauthorized.StatusCode {
return http.StatusUnauthorized
}
return http.StatusOK
}
// OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2.
// see https://github.com/owncloud/core/blob/c08baf580927ecb8ec179028dda255fdd85b4568/lib/private/legacy/api.php#L528
// also HTTP status codes for apps are the same as OCS codes
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
// I think this is a bug in the ocs v2 api, but since we are going to mimic bugs in ocis ... here goes
func OcsV2StatusCodes(meta data.Meta) int {
sc := meta.StatusCode
switch sc {
@@ -47,7 +54,8 @@ func OcsV2StatusCodes(meta data.Meta) int {
return http.StatusInternalServerError
case data.MetaUnauthorized.StatusCode:
return http.StatusUnauthorized
case 100:
case data.MetaOK.StatusCode:
// TODO mustn't data.Meta be a pointer so this assignment has an effect
meta.StatusCode = http.StatusOK
return http.StatusOK
}
@@ -61,7 +69,7 @@ func OcsV2StatusCodes(meta data.Meta) int {
return http.StatusBadRequest
}
// TODO change this status code?
// TODO change this status code? yes, align with oc10 core mapStatusCodes
return http.StatusOK
}
+83 -26
View File
@@ -2,18 +2,24 @@ package svc
import (
"net/http"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/micro/go-micro/v2/client/grpc"
mclient "github.com/micro/go-micro/v2/client"
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
"github.com/owncloud/ocis/ocis-pkg/account"
"github.com/owncloud/ocis/ocis-pkg/log"
opkgm "github.com/owncloud/ocis/ocis-pkg/middleware"
"github.com/owncloud/ocis/ocis-pkg/roles"
"github.com/owncloud/ocis/ocs/pkg/config"
ocsm "github.com/owncloud/ocis/ocs/pkg/middleware"
"github.com/owncloud/ocis/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/ocs/pkg/service/v0/response"
"github.com/owncloud/ocis/ocis-pkg/log"
settings "github.com/owncloud/ocis/settings/pkg/proto/v0"
)
var defaultClient = grpc.NewClient()
@@ -31,19 +37,47 @@ func NewService(opts ...Option) Service {
m := chi.NewMux()
m.Use(options.Middleware...)
svc := Ocs{
config: options.Config,
mux: m,
logger: options.Logger,
roleService := options.RoleService
if roleService == nil {
// https://github.com/owncloud/ocis-proxy/issues/38
// TODO this won't work with a registry other than mdns. Look into Micro's client initialization.
roleService = settings.NewRoleService("com.owncloud.api.settings", mclient.DefaultClient)
}
roleManager := options.RoleManager
if roleManager == nil {
m := roles.NewManager(
roles.CacheSize(1024),
roles.CacheTTL(time.Hour*24*7),
roles.Logger(options.Logger),
roles.RoleService(roleService),
)
roleManager = &m
}
svc := Ocs{
config: options.Config,
mux: m,
RoleManager: roleManager,
logger: options.Logger,
}
requireUser := ocsm.RequireUser()
requireAdmin := ocsm.RequireAdmin(
ocsm.RoleManager(roleManager),
)
requireSelfOrAdmin := ocsm.RequireSelfOrAdmin(
ocsm.RoleManager(roleManager),
ocsm.Logger(options.Logger),
)
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.NotFound(svc.NotFound)
r.Use(middleware.StripSlashes)
r.Use(ocsm.AccessToken(
ocsm.Logger(options.Logger),
ocsm.TokenManagerConfig(options.Config.TokenManager),
))
r.Use(opkgm.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret)),
)
r.Use(ocsm.OCSFormatCtx) // updates request Accept header according to format=(json|xml) query parameter
r.Route("/v{version:(1|2)}.php", func(r chi.Router) {
r.Use(response.VersionCtx) // stores version in context
@@ -51,32 +85,48 @@ func NewService(opts ...Option) Service {
r.Route("/apps/notifications/api/v1", func(r chi.Router) {})
r.Route("/cloud", func(r chi.Router) {
r.Route("/capabilities", func(r chi.Router) {})
// TODO /apps
r.Route("/user", func(r chi.Router) {
r.Get("/", svc.GetUser)
r.With(requireSelfOrAdmin).Get("/", svc.GetSelf)
r.Get("/signing-key", svc.GetSigningKey)
})
// for /users endpoints see https://github.com/owncloud/core/blob/master/apps/provisioning_api/appinfo/routes.php#L44-L56
r.Route("/users", func(r chi.Router) {
r.Get("/", svc.ListUsers)
r.Post("/", svc.AddUser)
r.Get("/{userid}", svc.GetUser)
r.Put("/{userid}", svc.EditUser)
r.Delete("/{userid}", svc.DeleteUser)
r.With(requireAdmin).Get("/", svc.ListUsers)
r.With(requireAdmin).Post("/", svc.AddUser)
r.Route("/{userid}", func(r chi.Router) {
r.With(requireUser).Get("/", svc.GetUser)
r.With(requireSelfOrAdmin).Put("/", svc.EditUser)
r.With(requireAdmin).Delete("/", svc.DeleteUser)
r.With(requireAdmin).Put("/enable", svc.EnableUser)
r.With(requireAdmin).Put("/disable", svc.DisableUser)
})
r.Route("/{userid}/groups", func(r chi.Router) {
r.Get("/", svc.ListUserGroups)
r.Post("/", svc.AddToGroup)
r.Delete("/", svc.RemoveFromGroup)
r.With(requireSelfOrAdmin).Get("/", svc.ListUserGroups)
r.With(requireAdmin).Post("/", svc.AddToGroup)
r.With(requireAdmin).Delete("/", svc.RemoveFromGroup)
})
r.Route("/{userid}/subadmins", func(r chi.Router) {
r.With(requireAdmin).Post("/", svc.NotImplementedStub)
r.With(requireSelfOrAdmin).Get("/", svc.NotImplementedStub)
r.With(requireAdmin).Delete("/", svc.NotImplementedStub)
})
})
// for /groups endpoints see https://github.com/owncloud/core/blob/master/apps/provisioning_api/appinfo/routes.php#L65-L69
r.Route("/groups", func(r chi.Router) {
r.Get("/", svc.ListGroups)
r.Post("/", svc.AddGroup)
r.Delete("/{groupid}", svc.DeleteGroup)
r.Get("/{groupid}", svc.GetGroupMembers)
r.With(requireAdmin).Get("/", svc.ListGroups)
r.With(requireAdmin).Post("/", svc.AddGroup)
r.With(requireSelfOrAdmin).Get("/{groupid}", svc.GetGroupMembers)
r.With(requireAdmin).Delete("/{groupid}", svc.DeleteGroup)
r.With(requireAdmin).Get("/{groupid}/subadmins", svc.NotImplementedStub)
})
})
r.Route("/config", func(r chi.Router) {
r.Get("/", svc.GetConfig)
r.With(requireUser).Get("/", svc.GetConfig)
})
})
})
@@ -86,9 +136,11 @@ func NewService(opts ...Option) Service {
// Ocs defines implements the business logic for Service.
type Ocs struct {
config *config.Config
logger log.Logger
mux *chi.Mux
config *config.Config
logger log.Logger
RoleService settings.RoleService
RoleManager *roles.Manager
mux *chi.Mux
}
// ServeHTTP implements the Service interface.
@@ -108,3 +160,8 @@ func (o Ocs) getAccountService() accounts.AccountsService {
func (o Ocs) getGroupsService() accounts.GroupsService {
return accounts.NewGroupsService("com.owncloud.api.accounts", defaultClient)
}
// NotImplementedStub returns a not implemented error
func (o Ocs) NotImplementedStub(w http.ResponseWriter, r *http.Request) {
render.Render(w, r, response.ErrRender(data.MetaUnknownError.StatusCode, "Not implemented"))
}
+143 -20
View File
@@ -12,6 +12,7 @@ import (
"github.com/cs3org/reva/pkg/user"
"github.com/go-chi/chi"
"github.com/go-chi/render"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/micro/go-micro/v2/client/grpc"
@@ -22,22 +23,58 @@ import (
storepb "github.com/owncloud/ocis/store/pkg/proto/v0"
)
// GetUser returns the currently logged in user
// GetSelf returns the currently logged in user
func (o Ocs) GetSelf(w http.ResponseWriter, r *http.Request) {
var account *accounts.Account
var err error
u, ok := user.ContextGetUser(r.Context())
if !ok || u.Id == nil || u.Id.OpaqueId == "" {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "user is missing an id"))
return
}
account, err = o.getAccountService().GetAccount(r.Context(), &accounts.GetAccountRequest{
Id: u.Id.OpaqueId,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
// if the user was authenticated why was he not found?!? log error?
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(merr).Interface("user", u).Msg("could not get account for user")
return
}
// remove password from log if it is set
if account.PasswordProfile != nil {
account.PasswordProfile.Password = ""
}
o.logger.Debug().Interface("account", account).Msg("got user")
d := &data.User{
UserID: account.Id,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
// TODO hide enabled flag or it might get rendered as false
}
render.Render(w, r, response.DataRender(d))
}
// GetUser returns the user with the given userid
func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) {
// TODO this endpoint needs authentication using the roles and permissions
userid := chi.URLParam(r, "userid")
var account *accounts.Account
var err error
if userid == "" {
u, ok := user.ContextGetUser(r.Context())
if !ok || u.Id == nil || u.Id.OpaqueId == "" {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context"))
return
}
account, err = o.getAccountService().GetAccount(r.Context(), &accounts.GetAccountRequest{
Id: u.Id.OpaqueId,
})
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context"))
} else {
account, err = o.fetchAccountByUsername(r.Context(), userid)
}
@@ -48,7 +85,7 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) {
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not get user")
o.logger.Error().Err(merr).Str("userid", userid).Msg("could not get account for user")
return
}
@@ -67,14 +104,13 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) {
}
d := &data.User{
UserID: account.PreferredName,
UserID: account.Id,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
Enabled: enabled,
// FIXME onlyfor users/{userid} endpoint (not /user)
Enabled: enabled, // TODO include in response only when admin?
// TODO query storage registry for free space? of home storage, maybe...
Quota: &data.Quota{
Free: 2840756224000,
@@ -89,7 +125,6 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) {
// AddUser creates a new user account
func (o Ocs) AddUser(w http.ResponseWriter, r *http.Request) {
// TODO this endpoint needs authentication using the roles and permissions
userid := r.PostFormValue("userid")
password := r.PostFormValue("password")
displayname := r.PostFormValue("displayname")
@@ -150,9 +185,18 @@ func (o Ocs) AddUser(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusBadRequest {
switch merr.Code {
case http.StatusBadRequest:
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
case http.StatusConflict:
if response.APIVersion(r.Context()) == "2" {
// it seems the application framework sets the ocs status code to the httpstatus code, which affects the provisioning api
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
render.Render(w, r, response.ErrRender(data.MetaInvalidInput.StatusCode, merr.Detail))
}
default:
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not add user")
@@ -186,7 +230,6 @@ func (o Ocs) AddUser(w http.ResponseWriter, r *http.Request) {
// EditUser creates a new user account
func (o Ocs) EditUser(w http.ResponseWriter, r *http.Request) {
// TODO this endpoint needs authentication
userid := chi.URLParam(r, "userid")
account, err := o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
@@ -239,7 +282,7 @@ func (o Ocs) EditUser(w http.ResponseWriter, r *http.Request) {
default:
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", req.Account.Id).Msg("could not edit user")
o.logger.Error().Err(err).Str("account_id", req.Account.Id).Str("user_id", userid).Msg("could not edit user")
return
}
@@ -287,6 +330,86 @@ func (o Ocs) DeleteUser(w http.ResponseWriter, r *http.Request) {
render.Render(w, r, response.DataRender(struct{}{}))
}
// EnableUser enables a user
func (o Ocs) EnableUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
account, err := o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not enable user")
return
}
account.AccountEnabled = true
req := accounts.UpdateAccountRequest{
Account: account,
UpdateMask: &field_mask.FieldMask{
Paths: []string{"AccountEnabled"},
},
}
_, err = o.getAccountService().UpdateAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested account could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("account_id", account.Id).Msg("could not enable account")
return
}
o.logger.Debug().Str("account_id", account.Id).Msg("enabled user")
render.Render(w, r, response.DataRender(struct{}{}))
}
// DisableUser disables a user
func (o Ocs) DisableUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
account, err := o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not disable user")
return
}
account.AccountEnabled = false
req := accounts.UpdateAccountRequest{
Account: account,
UpdateMask: &field_mask.FieldMask{
Paths: []string{"AccountEnabled"},
},
}
_, err = o.getAccountService().UpdateAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested account could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("account_id", account.Id).Msg("could not disable account")
return
}
o.logger.Debug().Str("account_id", account.Id).Msg("disabled user")
render.Render(w, r, response.DataRender(struct{}{}))
}
// GetSigningKey returns the signing key for the current user. It will create it on the fly if it does not exist
// The signing key is part of the user settings and is used by the proxy to authenticate requests
// Currently, the username is used as the OC-Credential
@@ -378,7 +501,7 @@ func (o Ocs) ListUsers(w http.ResponseWriter, r *http.Request) {
users := []string{}
for i := range res.Accounts {
users = append(users, res.Accounts[i].Id)
users = append(users, res.Accounts[i].OnPremisesSamAccountName)
}
render.Render(w, r, response.DataRender(&data.Users{Users: users}))
@@ -0,0 +1,6 @@
Enhancement: Add basic auth option
We added a new `enable-basic-auth` option and `PROXY_ENABLE_BASIC_AUTH` environment variable that can be set to `true` to make the proxy verify the basic auth header with the accounts service. This should only be used for testing and development and is disabled by default.
https://github.com/owncloud/ocis/pull/627
https://github.com/owncloud/product/issues/198
+107 -480
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -263,6 +263,8 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic
middleware.AccountsClient(accounts),
middleware.SettingsRoleService(roles),
middleware.AutoprovisionAccounts(cfg.AutoprovisionAccounts),
middleware.EnableBasicAuth(cfg.EnableBasicAuth),
middleware.OIDCIss(cfg.OIDC.Issuer),
)
// the connection will be established in a non blocking fashion
+1
View File
@@ -99,6 +99,7 @@ type Config struct {
Reva Reva
PreSignedURL PreSignedURL
AutoprovisionAccounts bool
EnableBasicAuth bool
}
// OIDC is the config for the OpenID-Connect middleware. If set the proxy will try to authenticate every request
+9
View File
@@ -219,6 +219,15 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
Usage: "--presignedurl-allow-method GET [--presignedurl-allow-method POST]",
EnvVars: []string{"PRESIGNEDURL_ALLOWED_METHODS"},
},
// Basic auth
&cli.BoolFlag{
Name: "enable-basic-auth",
Value: false,
Usage: "enable basic authentication",
EnvVars: []string{"PROXY_ENABLE_BASIC_AUTH"},
Destination: &cfg.EnableBasicAuth,
},
}
}
+33 -10
View File
@@ -72,6 +72,8 @@ func createAccount(l log.Logger, claims *oidc.StandardClaims, ac acc.AccountsSer
func AccountUUID(opts ...Option) func(next http.Handler) http.Handler {
opt := newOptions(opts...)
publicFilesEndpoint := "/remote.php/dav/public-files/"
return func(next http.Handler) http.Handler {
// TODO: handle error
tokenManager, err := jwt.New(map[string]interface{}{
@@ -85,22 +87,43 @@ func AccountUUID(opts ...Option) func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
l := opt.Logger
claims := oidc.FromContext(r.Context())
if claims == nil {
next.ServeHTTP(w, r)
return
}
var account *acc.Account
var status int
if claims.Email != "" {
switch {
case claims == nil:
login, password, ok := r.BasicAuth()
// check if we are dealing with a public link
if ok && login == "public" && strings.HasPrefix(r.URL.Path, publicFilesEndpoint) {
// forward to reva frontend
next.ServeHTTP(w, r)
return
}
if opt.EnableBasicAuth && ok {
l.Warn().Msg("basic auth enabled, use only for testing or development")
account, status = getAccount(l, opt.AccountsClient, fmt.Sprintf("login eq '%s' and password eq '%s'", strings.ReplaceAll(login, "'", "''"), strings.ReplaceAll(password, "'", "''")))
if status == 0 {
// fake claims for the subsequent code flow
claims = &oidc.StandardClaims{
Iss: opt.OIDCIss,
}
} else {
// tell client to reauthenticate
w.WriteHeader(http.StatusUnauthorized)
return
}
} else {
next.ServeHTTP(w, r)
return
}
case claims.Email != "":
account, status = getAccount(l, opt.AccountsClient, fmt.Sprintf("mail eq '%s'", strings.ReplaceAll(claims.Email, "'", "''")))
} else if claims.PreferredUsername != "" {
case claims.PreferredUsername != "":
account, status = getAccount(l, opt.AccountsClient, fmt.Sprintf("preferred_name eq '%s'", strings.ReplaceAll(claims.PreferredUsername, "'", "''")))
} else if claims.OcisID != "" {
case claims.OcisID != "":
account, status = getAccount(l, opt.AccountsClient, fmt.Sprintf("id eq '%s'", strings.ReplaceAll(claims.OcisID, "'", "''")))
} else {
default:
// TODO allow lookup by custom claim, eg an id ... or sub
l.Error().Err(err).Msgf("Could not lookup account, no mail or preferred_username claim set")
l.Error().Err(err).Msg("Could not lookup account, no mail or preferred_username claim set")
w.WriteHeader(http.StatusInternalServerError)
}
if status != 0 || account == nil {
+9
View File
@@ -39,6 +39,8 @@ type Options struct {
PreSignedURLConfig config.PreSignedURL
// AutoprovisionAccounts when an account does not exist.
AutoprovisionAccounts bool
// EnableBasicAuth to allow basic auth
EnableBasicAuth bool
}
// newOptions initializes the available default options.
@@ -128,3 +130,10 @@ func AutoprovisionAccounts(val bool) Option {
o.AutoprovisionAccounts = val
}
}
// EnableBasicAuth provides a function to set the EnableBasicAuth config
func EnableBasicAuth(enableBasicAuth bool) Option {
return func(o *Options) {
o.EnableBasicAuth = enableBasicAuth
}
}
+1 -1
View File
@@ -261,7 +261,7 @@ func defaultPolicies() []config.Policy {
},
{
Type: config.RegexRoute,
Endpoint: "/ocs/v[12].php/cloud/user", // we have `user` and `users` in ocis-ocs
Endpoint: "/ocs/v[12].php/cloud/(users?|groups)", // we have `user`, `users` and `groups` in ocis-ocs
Backend: "http://localhost:9110",
},
{
@@ -145,6 +145,9 @@ func TestProxyIntegration(t *testing.T) {
if err != nil {
t.Fatal("Error reading result body")
}
if err = rr.Result().Body.Close(); err != nil {
t.Fatal("Error closing result body")
}
bodyString := string(resultBody)
if bodyString != `OK` {