Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
SHELL := bash
NAME := auth-app
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
include ../../.make/default.mk
include ../../.make/go.mk
include ../../.make/release.mk
include ../../.make/docs.mk
+139
View File
@@ -0,0 +1,139 @@
# Auth-App
The auth-app service provides authentication for 3rd party apps unable to use
OpenID Connect. The service is enabled by default and started automatically. It
is possible to disable the service by setting:
```bash
OC_EXCLUDE_RUN_SERVICES=auth-app # deployment specific. Removes service from the list of automatically started services, use with single-binary deployments
PROXY_ENABLE_APP_AUTH=false # mandatory, disables app authentication. In case of a distributed environment, this envvar needs to be set in the proxy service.
```
## App Tokens
App Tokens are password specifically generated to be used by 3rd party applications
for authentication when accessing the КуСфера API endpoints. To
be able to use an app token, one must first create a token. There are different
options of creating a token.
## Important Security Note
When using an external IDP for authentication, App Token are NOT invalidated
when the user is disabled or locked in that external IDP. That means the user
will still be able to use its existing App Tokens for authentication for as
long as the App Tokes are valid.
## Managing App Tokens
### Via API
Please note: This API is preliminary. In the future we will provide endpoints
in the `graph` service for allowing the management of App Tokens.
The `auth-app` service provides an API to create (POST), list (GET) and delete (DELETE) tokens at the `/auth-app/tokens` endpoint.
* **Create a token**\
The POST request requires:
* A `expiry` key/value pair in the form of `expiry=<number><h|m|s>`\
Example: `expiry=72h`
```bash
curl --request POST 'https://<your host:9200>/auth-app/tokens?expiry={value}' \
--header 'accept: application/json'
```
Example output:
```
{
"token": "3s2K7816M4vuSpd5",
"expiration_date": "2024-08-08T13:42:42.796888022+02:00",
"created_date": "2024-08-07T13:42:42+02:00",
"label": "Generated via API"
}
```
Note, that this is the only time the app token will be returned in cleartext. To use the token
please copy it from the response.
* **List tokens**\
```bash
curl --request GET 'https://<your host:9200>/auth-app/tokens' \
--header 'accept: application/json'
```
Note that the `token` value in the response to the "List Tokens` request is not the actual
app token, but the UUID of the token. So this value cannot be used for authenticating
with the token.
Example output:
```
[
{
"token": "155f402e-1c5c-411c-92d4-92f3b612cd99"
"expiration_date": "2024-08-08T13:44:31.025199075+02:00",
"created_date": "2024-08-07T13:44:31+02:00",
"label": "Generated via Impersonation API"
},
{
"token": "8c606bdb-e22e-4094-9304-732fd4702bc9"
"expiration_date": "2024-08-08T13:46:41.936052281+02:00",
"created_date": "2024-08-07T13:46:42+02:00",
"label": "Generated via Impersonation API"
}
]
```
* **Delete a token**\
The DELETE request requires:
* A `token` key/value pair in the form of `token=<token_issued>`. The value needs to be the hashed value as returned by the `List Tokens` respone.\
Example: `token=8c606bdb-e22e-4094-9304-732fd4702bc9`
```bash
curl --request DELETE 'https://<your host:9200>/auth-app/tokens?token={value}' \
--header 'accept: application/json'
```
### Via Impersonation API
When setting the environment variable `AUTH_APP_ENABLE_IMPERSONATION` to
`true`, admins will be able to use the `/auth-app/tokens` endpoint to create
tokens for other users. This can be important for migration scenarios, but
should not be considered for regular tasks on a production system for security
reasons.
To impersonate, the respective requests from the CLI commands above extend with
the following parameters, where you can use one or the other:
* The `userID` in the form of: `userID={value}`\
Example:\
`userID=4c510ada- ... -42cdf82c3d51`
* The `userName` in the form of: `userName={value}`\
Example:\
`userName=alan`
Example:\
A final create request would then look like:
```bash
curl --request POST 'https://<your host:9200>/auth-app/tokens?expiry={value}&userName={value}' \
--header 'accept: application/json'
```
### Via CLI (developer only)
As the CLI is using the internal CS3Apis this needs access to the reva gateway
service. This is mainly of developer (and admin) usage.
Replace the `user-name` with an existing user. For the `token-expiration`, you
can use any time abbreviation from the following list: `h, m, s`. Examples:
`72h` or `1h` or `1m` or `1s.` Default is `72h`.
```bash
qsfera auth-app create --user-name={user-name} --expiration={token-expiration}
```
## Authenticating using App Tokens
To autenticate using an App Token simply use the username for which token was generated
and the token value as returned by the "Create Token" request.
```bash
curl -u <username>:<tokenvalue> 'https://<your host>/graph/v1.0/me' \
--header 'accept: application/json'
```
@@ -0,0 +1,124 @@
package command
import (
"context"
"fmt"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"github.com/spf13/cobra"
"time"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/services/auth-app/pkg/config"
"github.com/qsfera/server/services/auth-app/pkg/config/parser"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
applicationsv1beta1 "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
gatewayv1beta1 "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"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"google.golang.org/grpc/metadata"
)
// Create is the entrypoint for the app auth create command
func Create(cfg *config.Config) *cobra.Command {
createCmd := &cobra.Command{
Use: "create",
Short: "create an app auth token for a user",
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
traceProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
return err
}
gatewaySelector, err := pool.GatewaySelector(
cfg.Reva.Address,
append(
cfg.Reva.GetRevaOptions(),
pool.WithRegistry(registry.GetRegistry()),
pool.WithTracerProvider(traceProvider),
)...)
if err != nil {
return err
}
next, err := gatewaySelector.Next()
if err != nil {
return err
}
userName, _ := cmd.Flags().GetString("user-name")
if userName == "" {
fmt.Printf("Username to create app token for: ")
if _, err := fmt.Scanln(&userName); err != nil {
return err
}
}
ctx := context.Background()
authRes, err := next.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{
Type: "machine",
ClientId: "username:" + userName,
ClientSecret: cfg.MachineAuthAPIKey,
})
if err != nil {
return err
}
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return fmt.Errorf("error authenticating user: %s", authRes.GetStatus().GetMessage())
}
granteeCtx := ctxpkg.ContextSetUser(context.Background(), &userpb.User{Id: authRes.GetUser().GetId()})
granteeCtx = metadata.AppendToOutgoingContext(granteeCtx, ctxpkg.TokenHeader, authRes.GetToken())
scopes, err := scope.AddOwnerScope(map[string]*authpb.Scope{})
if err != nil {
return err
}
expiry, err := cmd.Flags().GetDuration("expiration")
if err != nil {
return err
}
appPassword, err := next.GenerateAppPassword(granteeCtx, &applicationsv1beta1.GenerateAppPasswordRequest{
TokenScope: scopes,
Label: "Generated via CLI",
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Add(expiry).Unix()),
},
})
if err != nil {
return err
}
fmt.Printf("App token created for %s", authRes.GetUser().GetUsername())
fmt.Println()
fmt.Printf(" token: %s", appPassword.GetAppPassword().GetPassword())
fmt.Println()
return nil
},
}
createCmd.Flags().String(
"user-name",
"",
"user to create the app-token for",
)
createCmd.Flags().Duration(
"expiration",
time.Hour*72,
"expiration of the app password, e.g. 72h, 1h, 1m, 1s. Default is 72h.",
)
return createCmd
}
@@ -0,0 +1,54 @@
package command
import (
"fmt"
"net/http"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/auth-app/pkg/config"
"github.com/qsfera/server/services/auth-app/pkg/config/parser"
"github.com/spf13/cobra"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "health",
Short: "check health status",
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}
@@ -0,0 +1,37 @@
package command
import (
"os"
"github.com/qsfera/server/pkg/clihelper"
"github.com/qsfera/server/services/auth-app/pkg/config"
"github.com/spf13/cobra"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) []*cobra.Command {
return []*cobra.Command{
// start this service
Server(cfg),
// interaction with this service
Create(cfg),
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the qsfera auth-app command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cobra.Command{
Use: "auth-app",
Short: "Provide app authentication for КуСфера",
})
app.AddCommand(GetCommands(cfg)...)
app.SetArgs(os.Args[1:])
return app.ExecuteContext(cfg.Context)
}
@@ -0,0 +1,144 @@
package command
import (
"context"
"fmt"
"os/signal"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/runner"
ogrpc "github.com/qsfera/server/pkg/service/grpc"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/pkg/version"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/auth-app/pkg/config"
"github.com/qsfera/server/services/auth-app/pkg/config/parser"
"github.com/qsfera/server/services/auth-app/pkg/revaconfig"
"github.com/qsfera/server/services/auth-app/pkg/server/debug"
"github.com/qsfera/server/services/auth-app/pkg/server/http"
"github.com/opencloud-eu/reva/v2/cmd/revad/runtime"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/spf13/cobra"
)
// Server is the entry point for the server command.
func Server(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "server",
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
if cfg.AllowImpersonation {
fmt.Println("WARNING: Impersonation is enabled. Admins can impersonate all users.")
}
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
traceProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
return err
}
var cancel context.CancelFunc
if cfg.Context == nil {
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
defer cancel()
}
ctx := cfg.Context
gr := runner.NewGroup()
{
// run the appropriate reva servers based on the config
rCfg := revaconfig.AuthAppConfigFromStruct(cfg)
if rServer := runtime.NewDrivenHTTPServerWithOptions(rCfg,
runtime.WithLogger(&logger.Logger),
runtime.WithRegistry(registry.GetRegistry()),
runtime.WithTraceProvider(traceProvider),
); rServer != nil {
gr.Add(runner.NewRevaServiceRunner(cfg.Service.Name+".rhttp", rServer))
}
if rServer := runtime.NewDrivenGRPCServerWithOptions(rCfg,
runtime.WithLogger(&logger.Logger),
runtime.WithRegistry(registry.GetRegistry()),
runtime.WithTraceProvider(traceProvider),
); rServer != nil {
gr.Add(runner.NewRevaServiceRunner(cfg.Service.Name+".rgrpc", rServer))
}
}
{
debugServer, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().Err(err).Str("server", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(runner.NewGolangHttpServerRunner("auth-app_debug", debugServer))
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, logger, grpcSvc, cfg.Debug.Addr); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
tm, err := pool.StringToTLSMode(cfg.GRPCClientTLS.Mode)
if err != nil {
return err
}
gatewaySelector, err := pool.GatewaySelector(
cfg.Reva.Address,
append(
cfg.Reva.GetRevaOptions(),
pool.WithTLSCACert(cfg.GRPCClientTLS.CACert),
pool.WithTLSMode(tm),
pool.WithRegistry(registry.GetRegistry()),
pool.WithTracerProvider(traceProvider),
)...)
if err != nil {
return err
}
grpcClient, err := ogrpc.NewClient(
append(ogrpc.GetClientOptions(cfg.GRPCClientTLS), ogrpc.WithTraceProvider(traceProvider))...,
)
if err != nil {
return err
}
{
rClient := settingssvc.NewRoleService("qsfera.api.settings", grpcClient)
server, err := http.Server(
http.Logger(logger),
http.Context(ctx),
http.Config(cfg),
http.GatewaySelector(gatewaySelector),
http.RoleClient(rClient),
http.TracerProvider(traceProvider),
)
if err != nil {
logger.Fatal().Err(err).Msg("failed to initialize http server")
}
gr.Add(runner.NewGoMicroHttpServerRunner("auth-app_http", server))
}
grResults := gr.Run(ctx)
// return the first non-nil error found in the results
for _, grResult := range grResults {
if grResult.RunnerError != nil {
return grResult.RunnerError
}
}
return nil
},
}
}
@@ -0,0 +1,49 @@
package command
import (
"fmt"
"os"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/auth-app/pkg/config"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "print the version of this binary and the running service instances",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.GRPC.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
@@ -0,0 +1,100 @@
package config
import (
"context"
"github.com/qsfera/server/pkg/shared"
)
// Config defines the root config structure
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;AUTH_APP_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
Debug Debug `yaml:"debug"`
GRPC GRPCConfig `yaml:"grpc"`
HTTP HTTP `yaml:"http"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
TokenManager *TokenManager `yaml:"token_manager"`
Reva *shared.Reva `yaml:"reva"`
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"AUTH_APP_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the encoding of the user's group memberships in the access token. This reduces the token size, especially when users are members of a large number of groups." introductionVersion:"1.0.0"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;AUTH_APP_MACHINE_AUTH_API_KEY" desc:"The machine auth API key used to validate internal requests necessary to access resources from other services." introductionVersion:"1.0.0"`
AllowImpersonation bool `yaml:"allow_impersonation" env:"AUTH_APP_ENABLE_IMPERSONATION" desc:"Allows admins to create app tokens for other users. Used for migration. Do NOT use in productive deployments." introductionVersion:"1.0.0"`
StorageDriver string `yaml:"storage_driver" env:"AUTH_APP_STORAGE_DRIVER" desc:"Driver to be used to persist the app tokes . Supported values are 'jsoncs3', 'json'." introductionVersion:"4.0.0"`
StorageDrivers StorageDrivers `yaml:"storage_drivers"`
Context context.Context `yaml:"-"`
}
type StorageDrivers struct {
JSONCS3 JSONCS3Driver `yaml:"jsoncs3"`
}
type JSONCS3Driver struct {
ProviderAddr string `yaml:"provider_addr" env:"AUTH_APP_JSONCS3_PROVIDER_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"4.0.0"`
SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;AUTH_APP_JSONCS3_SYSTEM_USER_ID" desc:"ID of the КуСфера STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"4.0.0"`
SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;AUTH_APP_JSONCS3_SYSTEM_USER_IDP" desc:"IDP of the КуСфера STORAGE-SYSTEM system user." introductionVersion:"4.0.0"`
SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY;AUTH_APP_JSONCS3_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"4.0.0"`
PasswordGenerator string `yaml:"password_generator" env:"AUTH_APP_JSONCS3_PASSWORD_GENERATOR" desc:"The password generator that should be used for generating app tokens. Supported values are: 'diceware' and 'random'." introductionVersion:"4.0.0"`
PasswordGeneratorOptions PasswordGeneratorOptions `yaml:"password_generator_options"`
}
type PasswordGeneratorOptions struct {
DicewareOptions DicewareOptions `yaml:"diceware"`
RandPWOpts RandPWOpts `yaml:"randon"`
}
// DicewareOptions defines the config options for the "diceware" password generator
type DicewareOptions struct {
NumberOfWords int `yaml:"number_of_words" env:"AUTH_APP_JSONCS3_DICEWARE_NUMBER_OF_WORDS" desc:"The number of words the generated passphrase will have." introductionVersion:"4.0.0"`
}
// RandPWOpts defines the config options for the "random" password generator
type RandPWOpts struct {
PasswordLength int `yaml:"password_length" env:"AUTH_APP_JSONCS3_RANDOM_PASSWORD_LENGTH" desc:"The number of charactors the generated passwords will have." introductionVersion:"4.0.0"`
}
// Service defines the service configuration
type Service struct {
Name string `yaml:"-"`
}
// Debug defines the debug configuration
type Debug struct {
Addr string `yaml:"addr" env:"AUTH_APP_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
Token string `yaml:"token" env:"AUTH_APP_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
Pprof bool `yaml:"pprof" env:"AUTH_APP_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
Zpages bool `yaml:"zpages" env:"AUTH_APP_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing traces in-memory." introductionVersion:"1.0.0"`
}
// GRPCConfig defines the GRPC configuration
type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_APP_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"1.0.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"OC_GRPC_PROTOCOL;AUTH_APP_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"1.0.0"`
}
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `yaml:"addr" env:"AUTH_APP_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"`
Namespace string `yaml:"-"`
Root string `yaml:"root" env:"AUTH_APP_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"`
CORS CORS `yaml:"cors"`
TLS shared.HTTPServiceTLS `yaml:"tls"`
}
// CORS defines the available cors configuration.
type CORS struct {
AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;AUTH_APP_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;AUTH_APP_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;AUTH_APP_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;AUTH_APP_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,113 @@
package defaults
import (
"strings"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/pkg/structs"
"github.com/qsfera/server/services/auth-app/pkg/config"
)
// FullDefaultConfig returns a fully initialized default configuration
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
// DefaultConfig returns a basic default configuration
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9245",
Token: "",
Pprof: false,
Zpages: false,
},
GRPC: config.GRPCConfig{
Addr: "127.0.0.1:9246",
Namespace: "qsfera.api",
Protocol: "tcp",
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9247",
Namespace: "qsfera.web",
Root: "/",
CORS: config.CORS{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "DELETE"},
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Ocs-Apirequest"},
AllowCredentials: true,
},
},
Service: config.Service{
Name: "auth-app",
},
StorageDriver: "jsoncs3",
StorageDrivers: config.StorageDrivers{
JSONCS3: config.JSONCS3Driver{
ProviderAddr: "qsfera.api.storage-system",
SystemUserIDP: "internal",
PasswordGenerator: "diceware",
PasswordGeneratorOptions: config.PasswordGeneratorOptions{
DicewareOptions: config.DicewareOptions{
NumberOfWords: 6,
},
},
},
},
Reva: shared.DefaultRevaConfig(),
}
}
// EnsureDefaults adds default values to the configuration if they are not set yet
func EnsureDefaults(cfg *config.Config) {
if cfg.LogLevel == "" {
cfg.LogLevel = "error"
}
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}
if cfg.Reva == nil && cfg.Commons != nil {
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
}
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
}
if cfg.StorageDrivers.JSONCS3.SystemUserAPIKey == "" && cfg.Commons != nil && cfg.Commons.SystemUserAPIKey != "" {
cfg.StorageDrivers.JSONCS3.SystemUserAPIKey = cfg.Commons.SystemUserAPIKey
}
if cfg.StorageDrivers.JSONCS3.SystemUserID == "" && cfg.Commons != nil && cfg.Commons.SystemUserID != "" {
cfg.StorageDrivers.JSONCS3.SystemUserID = cfg.Commons.SystemUserID
}
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}
if cfg.GRPC.TLS == nil && cfg.Commons != nil {
cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS)
}
if cfg.Commons != nil {
cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS
}
}
// Sanitize sanitized the configuration
func Sanitize(cfg *config.Config) {
// sanitize config
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
}
@@ -0,0 +1,42 @@
package parser
import (
"errors"
occfg "github.com/qsfera/server/pkg/config"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/services/auth-app/pkg/config"
"github.com/qsfera/server/services/auth-app/pkg/config/defaults"
"github.com/qsfera/server/pkg/config/envdecode"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
err := occfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
defaults.Sanitize(cfg)
return Validate(cfg)
}
func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
return nil
}
@@ -0,0 +1,6 @@
package config
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;AUTH_APP_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,73 @@
package revaconfig
import (
"path/filepath"
"github.com/qsfera/server/pkg/config/defaults"
"github.com/qsfera/server/services/auth-app/pkg/config"
)
// AuthAppConfigFromStruct will adapt a КуСфера config struct into a reva mapstructure to start a reva service.
func AuthAppConfigFromStruct(cfg *config.Config) map[string]any {
appAuthJSON := filepath.Join(defaults.BaseDataPath(), "appauth.json")
jsonCS3pwGenOpt := map[string]any{}
switch cfg.StorageDrivers.JSONCS3.PasswordGenerator {
case "random":
jsonCS3pwGenOpt["token_strength"] = cfg.StorageDrivers.JSONCS3.PasswordGeneratorOptions.RandPWOpts.PasswordLength
case "diceware":
jsonCS3pwGenOpt["number_of_words"] = cfg.StorageDrivers.JSONCS3.PasswordGeneratorOptions.DicewareOptions.NumberOfWords
}
rcfg := map[string]any{
"shared": map[string]any{
"jwt_secret": cfg.TokenManager.JWTSecret,
"gatewaysvc": cfg.Reva.Address,
"skip_user_groups_in_token": cfg.SkipUserGroupsInToken,
"grpc_client_options": cfg.Reva.GetGRPCClientConfig(),
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
},
"grpc": map[string]any{
"network": cfg.GRPC.Protocol,
"address": cfg.GRPC.Addr,
"tls_settings": map[string]any{
"enabled": cfg.GRPC.TLS.Enabled,
"certificate": cfg.GRPC.TLS.Cert,
"key": cfg.GRPC.TLS.Key,
},
"services": map[string]any{
"authprovider": map[string]any{
"auth_manager": "appauth",
"auth_managers": map[string]any{
"appauth": map[string]any{
"gateway_addr": cfg.Reva.Address,
},
},
},
"applicationauth": map[string]any{
"driver": cfg.StorageDriver,
"drivers": map[string]any{
"json": map[string]any{
"file": appAuthJSON,
},
"jsoncs3": map[string]any{
"provider_addr": cfg.StorageDrivers.JSONCS3.ProviderAddr,
"service_user_id": cfg.StorageDrivers.JSONCS3.SystemUserID,
"service_user_idp": cfg.StorageDrivers.JSONCS3.SystemUserIDP,
"machine_auth_apikey": cfg.StorageDrivers.JSONCS3.SystemUserAPIKey,
"password_generator": cfg.StorageDrivers.JSONCS3.PasswordGenerator,
"generator_config": jsonCS3pwGenOpt,
},
},
},
},
"interceptors": map[string]any{
"prometheus": map[string]any{
"namespace": "qsfera",
"subsystem": "auth_app",
},
},
},
}
return rcfg
}
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/auth-app/pkg/config"
)
// 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
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// 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
}
}
@@ -0,0 +1,27 @@
package debug
import (
"net/http"
"github.com/qsfera/server/pkg/service/debug"
"github.com/qsfera/server/pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
//debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
//debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
//debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
//debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
), nil
}
@@ -0,0 +1,96 @@
package http
import (
"context"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"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"
"github.com/spf13/pflag"
"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
Flags []pflag.Flag
Namespace string
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
RoleClient settingssvc.RoleService
TracerProvider trace.TracerProvider
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// 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
}
}
// Flags provides a function to set the flags option.
func Flags(flags ...pflag.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, flags...)
}
}
// Namespace provides a function to set the Namespace option.
func Namespace(val string) Option {
return func(o *Options) {
o.Namespace = 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
}
}
// RoleClient adds a grpc client for the role service
func RoleClient(rs settingssvc.RoleService) Option {
return func(o *Options) {
o.RoleClient = rs
}
}
// TracerProvider provides a function to set the TracerProvider option
func TracerProvider(val trace.TracerProvider) Option {
return func(o *Options) {
o.TracerProvider = val
}
}
@@ -0,0 +1,97 @@
package http
import (
"fmt"
stdhttp "net/http"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/qsfera/server/pkg/account"
"github.com/qsfera/server/pkg/cors"
"github.com/qsfera/server/pkg/middleware"
"github.com/qsfera/server/pkg/service/http"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/pkg/version"
svc "github.com/qsfera/server/services/auth-app/pkg/service"
"github.com/riandyrn/otelchi"
"go-micro.dev/v4"
)
// Service is the service interface
type Service any
// Server initializes the http service and server.
func Server(opts ...Option) (http.Service, error) {
options := newOptions(opts...)
service, err := http.NewService(
http.TLSConfig(options.Config.HTTP.TLS),
http.Logger(options.Logger),
http.Namespace(options.Config.HTTP.Namespace),
http.Name(options.Config.Service.Name),
http.Version(version.GetString()),
http.Address(options.Config.HTTP.Addr),
http.Context(options.Context),
http.Flags(options.Flags...),
http.TraceProvider(options.TracerProvider),
)
if err != nil {
options.Logger.Error().
Err(err).
Msg("Error initializing http service")
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
}
middlewares := []func(stdhttp.Handler) stdhttp.Handler{
chimiddleware.RequestID,
middleware.Version(
options.Config.Service.Name,
version.GetString(),
),
middleware.Logger(
options.Logger,
),
middleware.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret),
),
middleware.Cors(
cors.Logger(options.Logger),
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
),
}
mux := chi.NewMux()
mux.Use(middlewares...)
mux.Use(
otelchi.Middleware(
"auth-app",
otelchi.WithChiRoutes(mux),
otelchi.WithTracerProvider(options.TracerProvider),
otelchi.WithPropagators(tracing.GetPropagator()),
),
)
handle, err := svc.NewAuthAppService(
svc.Logger(options.Logger),
svc.Mux(mux),
svc.Config(options.Config),
svc.GatewaySelector(options.GatewaySelector),
svc.RoleClient(options.RoleClient),
svc.TraceProvider(options.TracerProvider),
)
if err != nil {
return http.Service{}, err
}
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
return http.Service{}, err
}
return service, nil
}
@@ -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(),
}
}