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
-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}))