Initial QSfera import
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
"github.com/qsfera/server/services/auth-app/pkg/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
Mux *chi.Mux
|
||||
TracerProvider trace.TracerProvider
|
||||
RoleClient settingssvc.RoleService
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// GatewaySelector provides a function to configure the gateway client selector
|
||||
func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) Option {
|
||||
return func(o *Options) {
|
||||
o.GatewaySelector = gatewaySelector
|
||||
}
|
||||
}
|
||||
|
||||
// TraceProvider provides a function to set the TracerProvider option
|
||||
func TraceProvider(val trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TracerProvider = val
|
||||
}
|
||||
}
|
||||
|
||||
// Mux defines the muxer for the userlog service
|
||||
func Mux(m *chi.Mux) Option {
|
||||
return func(o *Options) {
|
||||
o.Mux = m
|
||||
}
|
||||
}
|
||||
|
||||
// RoleClient adds a grpc client for the role service
|
||||
func RoleClient(rs settingssvc.RoleService) Option {
|
||||
return func(o *Options) {
|
||||
o.RoleClient = rs
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
applications "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
|
||||
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/roles"
|
||||
"github.com/qsfera/server/services/auth-app/pkg/config"
|
||||
settings "github.com/qsfera/server/services/settings/pkg/service/v0"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
var ErrBadRequest = errors.New("bad request")
|
||||
|
||||
// AuthAppToken represents an app token.
|
||||
type AuthAppToken struct {
|
||||
Token string `json:"token"`
|
||||
ExpirationDate time.Time `json:"expiration_date"`
|
||||
CreatedDate time.Time `json:"created_date"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// AuthAppService defines the service interface.
|
||||
type AuthAppService struct {
|
||||
log log.Logger
|
||||
cfg *config.Config
|
||||
gws pool.Selectable[gateway.GatewayAPIClient]
|
||||
m *chi.Mux
|
||||
r *roles.Manager
|
||||
}
|
||||
|
||||
// NewAuthAppService initializes a new AuthAppService.
|
||||
func NewAuthAppService(opts ...Option) (*AuthAppService, error) {
|
||||
o := &Options{}
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
|
||||
r := roles.NewManager(
|
||||
roles.Logger(o.Logger),
|
||||
roles.RoleService(o.RoleClient),
|
||||
)
|
||||
|
||||
a := &AuthAppService{
|
||||
log: o.Logger,
|
||||
cfg: o.Config,
|
||||
gws: o.GatewaySelector,
|
||||
m: o.Mux,
|
||||
r: &r,
|
||||
}
|
||||
|
||||
a.m.Route("/auth-app/tokens", func(r chi.Router) {
|
||||
r.Get("/", a.HandleList)
|
||||
r.Post("/", a.HandleCreate)
|
||||
r.Delete("/", a.HandleDelete)
|
||||
})
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// ServeHTTP implements the http.Handler interface.
|
||||
func (a *AuthAppService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.m.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// HandleCreate handles the creation of app tokens
|
||||
func (a *AuthAppService) HandleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := getContext(r)
|
||||
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()
|
||||
|
||||
gwc, err := a.gws.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error getting gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
expiry, err := time.ParseDuration(q.Get("expiry"))
|
||||
if err != nil {
|
||||
sublog.Info().Err(err).Str("duration", q.Get("expiry")).Msg("error parsing expiry")
|
||||
http.Error(w, "error parsing expiry. Use e.g. 30m or 72h", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
label := q.Get("label")
|
||||
if label == "" {
|
||||
label = "Generated via API"
|
||||
}
|
||||
|
||||
// Impersonated request
|
||||
userID, userName := q.Get("userID"), q.Get("userName")
|
||||
if userID != "" || userName != "" {
|
||||
if !a.cfg.AllowImpersonation {
|
||||
sublog.Error().Msg("impersonation is not allowed")
|
||||
http.Error(w, "impersonation is not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
ok, err := isAdmin(ctx, a.r)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error checking if user is admin")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
sublog.Error().Msg("user is not admin")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
ctx, err = a.authenticateUser(userID, userName, gwc)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error authenticating user")
|
||||
if errors.Is(err, ErrBadRequest) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
label = label + " (Impersonation)"
|
||||
}
|
||||
|
||||
scopes, err := scope.AddOwnerScope(map[string]*authpb.Scope{})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error adding owner scope")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := gwc.GenerateAppPassword(ctx, &applications.GenerateAppPasswordRequest{
|
||||
TokenScope: scopes,
|
||||
Label: label,
|
||||
Expiration: utils.TimeToTS(time.Now().Add(expiry)),
|
||||
})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error generating app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error generating app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(convert(res.GetAppPassword()))
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error marshaling app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(b); err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing response")
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
// HandleList handles listing of app tokens
|
||||
func (a *AuthAppService) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := getContext(r)
|
||||
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()
|
||||
|
||||
gwc, err := a.gws.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error getting gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := gwc.ListAppPasswords(ctx, &applications.ListAppPasswordsRequest{})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error listing app passwords")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error listing app passwords")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tokens := make([]AuthAppToken, 0, len(res.GetAppPasswords()))
|
||||
for _, ap := range res.GetAppPasswords() {
|
||||
tokens = append(tokens, convert(ap))
|
||||
}
|
||||
|
||||
b, err := json.Marshal(tokens)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error marshaling app passwords")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(b); err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleDelete handles deletion of app tokens
|
||||
func (a *AuthAppService) HandleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := getContext(r)
|
||||
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()
|
||||
|
||||
gwc, err := a.gws.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error getting gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
pw := r.URL.Query().Get("token")
|
||||
if pw == "" {
|
||||
sublog.Info().Msg("missing token")
|
||||
http.Error(w, "missing auth-app token. Set 'token' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := gwc.InvalidateAppPassword(ctx, &applications.InvalidateAppPasswordRequest{Password: pw})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error invalidating app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error invalidating app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *AuthAppService) authenticateUser(userID, userName string, gwc gateway.GatewayAPIClient) (context.Context, error) {
|
||||
ctx := context.Background()
|
||||
authRes, err := gwc.Authenticate(ctx, &gateway.AuthenticateRequest{
|
||||
Type: "machine",
|
||||
ClientId: buildClientID(userID, userName),
|
||||
ClientSecret: a.cfg.MachineAuthAPIKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, errors.New("error authenticating user: " + authRes.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
if (userID != "" && authRes.GetUser().GetId().GetOpaqueId() != userID) || (userName != "" && authRes.GetUser().GetUsername() != userName) {
|
||||
return nil, fmt.Errorf("requested user does not match authenticated user: userID:%s, userName:%s, %w", authRes.GetUser().GetId().GetOpaqueId(), authRes.GetUser().GetUsername(), ErrBadRequest)
|
||||
}
|
||||
|
||||
ctx = ctxpkg.ContextSetUser(ctx, &userpb.User{Id: authRes.GetUser().GetId()})
|
||||
return metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.GetToken()), nil
|
||||
}
|
||||
|
||||
func getContext(r *http.Request) context.Context {
|
||||
ctx := r.Context()
|
||||
return metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, r.Header.Get(ctxpkg.TokenHeader))
|
||||
}
|
||||
|
||||
func buildClientID(userID, userName string) string {
|
||||
switch {
|
||||
default:
|
||||
return ""
|
||||
case userID != "":
|
||||
return "userid:" + userID
|
||||
case userName != "":
|
||||
return "username:" + userName
|
||||
}
|
||||
}
|
||||
|
||||
// isAdmin determines if the user in the context is an admin / has account management permissions
|
||||
func isAdmin(ctx context.Context, rm *roles.Manager) (bool, error) {
|
||||
logger := appctx.GetLogger(ctx)
|
||||
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
uid := u.GetId().GetOpaqueId()
|
||||
if !ok || uid == "" {
|
||||
logger.Error().Str("userid", uid).Msg("user not in context")
|
||||
return false, errors.New("no user in context")
|
||||
}
|
||||
// get roles from context
|
||||
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
|
||||
if !ok {
|
||||
logger.Debug().Str("userid", uid).Msg("No roles in context, contacting settings service")
|
||||
var err error
|
||||
roleIDs, err = rm.FindRoleIDsForUser(ctx, uid)
|
||||
if err != nil {
|
||||
logger.Err(err).Str("userid", uid).Msg("failed to get roles for user")
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(roleIDs) == 0 {
|
||||
logger.Err(err).Str("userid", uid).Msg("user has no roles")
|
||||
return false, errors.New("user has no roles")
|
||||
}
|
||||
}
|
||||
|
||||
// check if permission is present in roles of the authenticated account
|
||||
return rm.FindPermissionByID(ctx, roleIDs, settings.AccountManagementPermissionID) != nil, nil
|
||||
}
|
||||
|
||||
func convert(ap *applications.AppPassword) AuthAppToken {
|
||||
return AuthAppToken{
|
||||
Token: ap.GetPassword(),
|
||||
ExpirationDate: utils.TSToTime(ap.GetExpiration()),
|
||||
CreatedDate: utils.TSToTime(ap.GetCtime()),
|
||||
Label: ap.GetLabel(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user