Merge pull request #3329 from opencloud-eu/chore/bump-reva

chore: bump reva to latest main
This commit is contained in:
Andre Duffeck authored and GitHub committed 2026-08-19 15:54:30 +02:00
commit 5f1a3ee869
42 files changed
+695 -17509

No files matched your search

+1
View File
@@ -2473,6 +2473,7 @@ def opencloudServer(storage = "decomposed", depends_on = [], deploy_type = "", e
"EVENTHISTORY_STORE": "memory",
"OC_TRANSLATION_PATH": "%s/tests/config/translations" % dirs["base"],
"ACTIVITYLOG_WRITE_BUFFER_DURATION": "0", # Disable write buffer so that test expectations are met in time
"OC_LDAP_LOOKUP_CACHE_TTL": "0", # disable ldap lookup cache so that test fixture change are applied right away
# search grpc port needed for index cli tests
"SEARCH_GRPC_ADDR": "0.0.0.0:9220",
# debug addresses required for running services health tests
+2 -2
View File
@@ -64,7 +64,7 @@ require (
github.com/open-policy-agent/opa v1.19.0
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260811071808-9bf68d2ccd96
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260819125853-c9416ac8b8c0
github.com/opensearch-project/opensearch-go/v4 v4.6.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
@@ -94,7 +94,7 @@ require (
github.com/xhit/go-simple-mail/v2 v2.16.0
go-micro.dev/v4 v4.11.0
go.etcd.io/bbolt v1.5.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0
go.opentelemetry.io/contrib/zpages v0.69.0
go.opentelemetry.io/otel v1.45.0
+4 -4
View File
@@ -942,8 +942,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55 h1:dzYZ5iA6i0QoFUap9l6sZ4Ts2HXiXVfsOoiRYToYgm8=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260811071808-9bf68d2ccd96 h1:2yPjf1jhrhEkrHsX8s6gAQVogBmNY6ZTtPNOtLfc1p8=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260811071808-9bf68d2ccd96/go.mod h1:+2IJFVwi3yBEBfw0T+jgY7M9LBSPv7ecPYDiG3/bWYg=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260819125853-c9416ac8b8c0 h1:IYqEof5RbfOrPE0g3FH7paBHNTpicB08G0Fy8lagnRc=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260819125853-c9416ac8b8c0/go.mod h1:OesdFn3szHcO/gifHWjbMkbkH6tgggkXL3KaNCI81ko=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -1293,8 +1293,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 h1:oECp5f+hN7nkwjU/8BxQ/q23bGPb8FIrD839owX222E=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0/go.mod h1:DqEFwLumhzMBDQv9PcWbyoDxHI/4lAk6CM4nJBH39sc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
go.opentelemetry.io/contrib/zpages v0.69.0 h1:YQC1PumJq6lUGQNrLW14ID9a0dXSBcbC/aC6VRSIARU=
+2
View File
@@ -2,6 +2,7 @@ package config
import (
"context"
"time"
"github.com/opencloud-eu/opencloud/pkg/shared"
)
@@ -72,6 +73,7 @@ type LDAPProvider struct {
LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"OC_LDAP_DISABLED_USERS_GROUP_DN;AUTH_BASIC_DISABLED_USERS_GROUP_DN" desc:"The distinguished name of the group to which added users will be classified as disabled when 'disable_user_mechanism' is set to 'group'." introductionVersion:"1.0.0"`
UserSchema LDAPUserSchema `yaml:"user_schema"`
GroupSchema LDAPGroupSchema `yaml:"group_schema"`
LookupCacheTTL time.Duration `yaml:"lookup_cache_ttl" env:"OC_LDAP_LOOKUP_CACHE_TTL;AUTH_BASIC_LDAP_LOOKUP_CACHE_TTL" desc:"The time to live for the in-memory cache of LDAP lookups. This cache is used to reduce the number of LDAP queries for user and group lookups." introductionVersion:"%%NEXT%%"`
}
type LDAPUserSchema struct {
@@ -1,6 +1,8 @@
package defaults
import (
"time"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config"
@@ -64,6 +66,7 @@ func DefaultConfig() *config.Config {
Groupname: "cn",
Member: "member",
},
LookupCacheTTL: 10 * time.Second,
},
JSON: config.JSONProvider{},
OwnCloudSQL: config.OwnCloudSQLProvider{
@@ -92,5 +92,6 @@ func ldapConfigFromString(cfg config.LDAPProvider) map[string]any {
"groupName": cfg.GroupSchema.Groupname,
"member": cfg.GroupSchema.Member,
},
"lookup_cache_ttl": cfg.LookupCacheTTL,
}
}
+2
View File
@@ -2,6 +2,7 @@ package config
import (
"context"
"time"
"github.com/opencloud-eu/opencloud/pkg/shared"
)
@@ -69,6 +70,7 @@ type LDAPDriver struct {
IDP string `yaml:"idp" env:"OC_URL;OC_OIDC_ISSUER;GROUPS_IDP_URL" desc:"The identity provider value to set in the group IDs of the CS3 group objects for groups returned by this group provider." introductionVersion:"1.0.0"`
UserSchema LDAPUserSchema `yaml:"user_schema"`
GroupSchema LDAPGroupSchema `yaml:"group_schema"`
LookupCacheTTL time.Duration `yaml:"lookup_cache_ttl" env:"OC_LDAP_LOOKUP_CACHE_TTL;GROUPS_LDAP_LOOKUP_CACHE_TTL" desc:"The time to live for the in-memory cache of LDAP lookups. This cache is used to reduce the number of LDAP queries for user and group lookups." introductionVersion:"%%NEXT%%"`
}
type LDAPUserSchema struct {
@@ -1,6 +1,8 @@
package defaults
import (
"time"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/groups/pkg/config"
@@ -61,6 +63,7 @@ func DefaultConfig() *config.Config {
Groupname: "cn",
Member: "member",
},
LookupCacheTTL: 10 * time.Second,
},
OwnCloudSQL: config.OwnCloudSQLDriver{
DBUsername: "owncloud",
+1
View File
@@ -87,5 +87,6 @@ func ldapConfigFromString(cfg config.LDAPDriver) map[string]any {
"groupName": cfg.GroupSchema.Groupname,
"member": cfg.GroupSchema.Member,
},
"lookup_cache_ttl": cfg.LookupCacheTTL,
}
}
+28 -24
View File
@@ -27,17 +27,19 @@ import (
// Session contains the information of an upload session
type Session struct {
ID string `json:"id"`
Space string `json:"space"`
Filename string `json:"filename"`
Offset int64 `json:"offset"`
Size int64 `json:"size"`
Executant userpb.UserId `json:"executant"`
SpaceOwner *userpb.UserId `json:"spaceowner,omitempty"`
Expires time.Time `json:"expires"`
Processing bool `json:"processing"`
ScanDate time.Time `json:"virus_scan_date"`
ScanResult string `json:"virus_scan_result"`
ID string `json:"id"`
Space string `json:"space"`
Filename string `json:"filename"`
Offset int64 `json:"offset"`
Size int64 `json:"size"`
Executant userpb.UserId `json:"executant"`
SpaceOwner *userpb.UserId `json:"spaceowner,omitempty"`
Expires time.Time `json:"expires"`
Processing bool `json:"processing"`
ScanDate time.Time `json:"virus_scan_date"`
ScanResult string `json:"virus_scan_result"`
Status string `json:"status"`
StatusMessage string `json:"status_message"`
}
// Uploads is the entry point for the uploads command
@@ -123,7 +125,7 @@ func ListUploadSessions(cfg *config.Config) *cobra.Command {
fmt.Println(buildInfo(filter))
table = tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Space", "Upload Id", "Name", "Offset", "Size", "Executant", "Owner", "Expires", "Processing", "Scan Date", "Scan Result"})
table.Header([]string{"Space", "Upload Id", "Name", "Status", "Message", "Offset", "Size", "Executant", "Owner", "Expires", "Scan Date", "Scan Result"})
}
for _, u := range uploads {
@@ -131,17 +133,18 @@ func ListUploadSessions(cfg *config.Config) *cobra.Command {
sr, sd := u.ScanData()
session := Session{
Space: ref.GetResourceId().GetSpaceId(),
ID: u.ID(),
Filename: u.Filename(),
Offset: u.Offset(),
Size: u.Size(),
Executant: u.Executant(),
SpaceOwner: u.SpaceOwner(),
Expires: u.Expires(),
Processing: u.IsProcessing(),
ScanDate: sd,
ScanResult: sr,
Space: ref.GetResourceId().GetSpaceId(),
ID: u.ID(),
Filename: u.Filename(),
Offset: u.Offset(),
Size: u.Size(),
Executant: u.Executant(),
SpaceOwner: u.SpaceOwner(),
Expires: u.Expires(),
ScanDate: sd,
ScanResult: sr,
Status: string(u.Status()),
StatusMessage: u.StatusMessage(),
}
if renderJson {
@@ -151,12 +154,13 @@ func ListUploadSessions(cfg *config.Config) *cobra.Command {
session.Space,
session.ID,
session.Filename,
session.Status,
session.StatusMessage,
strconv.FormatInt(session.Offset, 10),
strconv.FormatInt(session.Size, 10),
session.Executant.OpaqueId,
session.SpaceOwner.GetOpaqueId(),
session.Expires.Format(time.RFC3339),
strconv.FormatBool(session.Processing),
session.ScanDate.Format(time.RFC3339),
session.ScanResult,
})
+22 -20
View File
@@ -2,6 +2,7 @@ package config
import (
"context"
"time"
"github.com/opencloud-eu/opencloud/pkg/shared"
)
@@ -55,31 +56,32 @@ type JSONDriver struct {
File string `yaml:"file"`
}
type LDAPDriver struct {
URI string `yaml:"uri" env:"OC_LDAP_URI;USERS_LDAP_URI" desc:"URI of the LDAP Server to connect to. Supported URI schemes are 'ldaps://' and 'ldap://'" introductionVersion:"1.0.0"`
CACert string `yaml:"ca_cert" env:"OC_LDAP_CACERT;USERS_LDAP_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
Insecure bool `yaml:"insecure" env:"OC_LDAP_INSECURE;USERS_LDAP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments." introductionVersion:"1.0.0"`
BindDN string `yaml:"bind_dn" env:"OC_LDAP_BIND_DN;USERS_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server." introductionVersion:"1.0.0"`
BindPassword string `yaml:"bind_password" env:"OC_LDAP_BIND_PASSWORD;USERS_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'." introductionVersion:"1.0.0"`
UserBaseDN string `yaml:"user_base_dn" env:"OC_LDAP_USER_BASE_DN;USERS_LDAP_USER_BASE_DN" desc:"Search base DN for looking up LDAP users." introductionVersion:"1.0.0"`
GroupBaseDN string `yaml:"group_base_dn" env:"OC_LDAP_GROUP_BASE_DN;USERS_LDAP_GROUP_BASE_DN" desc:"Search base DN for looking up LDAP groups." introductionVersion:"1.0.0"`
URI string `yaml:"uri" env:"OC_LDAP_URI;USERS_LDAP_URI" desc:"URI of the LDAP Server to connect to. Supported URI schemes are 'ldaps://' and 'ldap://'" introductionVersion:"1.0.0"`
CACert string `yaml:"ca_cert" env:"OC_LDAP_CACERT;USERS_LDAP_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
Insecure bool `yaml:"insecure" env:"OC_LDAP_INSECURE;USERS_LDAP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments." introductionVersion:"1.0.0"`
BindDN string `yaml:"bind_dn" env:"OC_LDAP_BIND_DN;USERS_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server." introductionVersion:"1.0.0"`
BindPassword string `yaml:"bind_password" env:"OC_LDAP_BIND_PASSWORD;USERS_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'." introductionVersion:"1.0.0"`
UserBaseDN string `yaml:"user_base_dn" env:"OC_LDAP_USER_BASE_DN;USERS_LDAP_USER_BASE_DN" desc:"Search base DN for looking up LDAP users." introductionVersion:"1.0.0"`
GroupBaseDN string `yaml:"group_base_dn" env:"OC_LDAP_GROUP_BASE_DN;USERS_LDAP_GROUP_BASE_DN" desc:"Search base DN for looking up LDAP groups." introductionVersion:"1.0.0"`
TenantBaseDN string `yaml:"tenant_base_dn" env:"OC_LDAP_TENANT_BASE_DN;USERS_LDAP_TENANT_BASE_DN" desc:"Search base DN for looking up LDAP tenants. Only relevant in multi-tenant setups." introductionVersion:"6.1.0"`
TenantScope string `yaml:"tenant_scope" env:"OC_LDAP_TENANT_SCOPE;USERS_LDAP_TENANT_SCOPE" desc:"LDAP search scope to use when looking up tenants. Supported values are 'base', 'one' and 'sub'. Only relevant in multi-tenant setups." introductionVersion:"6.1.0"`
TenantFilter string `yaml:"tenant_filter" env:"OC_LDAP_TENANT_FILTER;USERS_LDAP_TENANT_FILTER" desc:"LDAP filter to add to the default filters for tenant searches. Only relevant in multi-tenant setups." introductionVersion:"6.1.0"`
TenantObjectClass string `yaml:"tenant_object_class" env:"OC_LDAP_TENANT_OBJECTCLASS;USERS_LDAP_TENANT_OBJECTCLASS" desc:"The object class to use for tenants in the default tenant search filter. Only relevant in multi-tenant setups." introductionVersion:"6.1.0"`
TenantSchema LDAPTenantSchema `yaml:"tenant_schema"`
UserScope string `yaml:"user_scope" env:"OC_LDAP_USER_SCOPE;USERS_LDAP_USER_SCOPE" desc:"LDAP search scope to use when looking up users. Supported values are 'base', 'one' and 'sub'." introductionVersion:"1.0.0"`
GroupScope string `yaml:"group_scope" env:"OC_LDAP_GROUP_SCOPE;USERS_LDAP_GROUP_SCOPE" desc:"LDAP search scope to use when looking up groups. Supported values are 'base', 'one' and 'sub'." introductionVersion:"1.0.0"`
UserSubstringFilterType string `yaml:"user_substring_filter_type" env:"LDAP_USER_SUBSTRING_FILTER_TYPE;USERS_LDAP_USER_SUBSTRING_FILTER_TYPE" desc:"Type of substring search filter to use for substring searches for users. Possible values: 'initial' for doing prefix only searches, 'final' for doing suffix only searches or 'any' for doing full substring searches" introductionVersion:"1.0.0"`
UserFilter string `yaml:"user_filter" env:"OC_LDAP_USER_FILTER;USERS_LDAP_USER_FILTER" desc:"LDAP filter to add to the default filters for user search like '(objectclass=openCloudUser)'." introductionVersion:"1.0.0"`
GroupFilter string `yaml:"group_filter" env:"OC_LDAP_GROUP_FILTER;USERS_LDAP_GROUP_FILTER" desc:"LDAP filter to add to the default filters for group searches." introductionVersion:"1.0.0"`
UserObjectClass string `yaml:"user_object_class" env:"OC_LDAP_USER_OBJECTCLASS;USERS_LDAP_USER_OBJECTCLASS" desc:"The object class to use for users in the default user search filter like 'inetOrgPerson'." introductionVersion:"1.0.0"`
GroupObjectClass string `yaml:"group_object_class" env:"OC_LDAP_GROUP_OBJECTCLASS;USERS_LDAP_GROUP_OBJECTCLASS" desc:"The object class to use for groups in the default group search filter like 'groupOfNames'." introductionVersion:"1.0.0"`
IDP string `yaml:"idp" env:"OC_URL;OC_OIDC_ISSUER;USERS_IDP_URL" desc:"The identity provider value to set in the userids of the CS3 user objects for users returned by this user provider." introductionVersion:"1.0.0"`
DisableUserMechanism string `yaml:"disable_user_mechanism" env:"OC_LDAP_DISABLE_USER_MECHANISM;USERS_LDAP_DISABLE_USER_MECHANISM" desc:"An option to control the behavior for disabling users. Valid options are 'none', 'attribute' and 'group'. If set to 'group', disabling a user via API will add the user to the configured group for disabled users, if set to 'attribute' this will be done in the ldap user entry, if set to 'none' the disable request is not processed." introductionVersion:"1.0.0"`
UserTypeAttribute string `yaml:"user_type_attribute" env:"OC_LDAP_USER_SCHEMA_USER_TYPE;USERS_LDAP_USER_TYPE_ATTRIBUTE" desc:"LDAP Attribute to distinguish between 'Member' and 'Guest' users. Default is 'openCloudUserType'." introductionVersion:"1.0.0"`
LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"OC_LDAP_DISABLED_USERS_GROUP_DN;USERS_LDAP_DISABLED_USERS_GROUP_DN" desc:"The distinguished name of the group to which added users will be classified as disabled when 'disable_user_mechanism' is set to 'group'." introductionVersion:"1.0.0"`
UserSchema LDAPUserSchema `yaml:"user_schema"`
GroupSchema LDAPGroupSchema `yaml:"group_schema"`
UserScope string `yaml:"user_scope" env:"OC_LDAP_USER_SCOPE;USERS_LDAP_USER_SCOPE" desc:"LDAP search scope to use when looking up users. Supported values are 'base', 'one' and 'sub'." introductionVersion:"1.0.0"`
GroupScope string `yaml:"group_scope" env:"OC_LDAP_GROUP_SCOPE;USERS_LDAP_GROUP_SCOPE" desc:"LDAP search scope to use when looking up groups. Supported values are 'base', 'one' and 'sub'." introductionVersion:"1.0.0"`
UserSubstringFilterType string `yaml:"user_substring_filter_type" env:"LDAP_USER_SUBSTRING_FILTER_TYPE;USERS_LDAP_USER_SUBSTRING_FILTER_TYPE" desc:"Type of substring search filter to use for substring searches for users. Possible values: 'initial' for doing prefix only searches, 'final' for doing suffix only searches or 'any' for doing full substring searches" introductionVersion:"1.0.0"`
UserFilter string `yaml:"user_filter" env:"OC_LDAP_USER_FILTER;USERS_LDAP_USER_FILTER" desc:"LDAP filter to add to the default filters for user search like '(objectclass=openCloudUser)'." introductionVersion:"1.0.0"`
GroupFilter string `yaml:"group_filter" env:"OC_LDAP_GROUP_FILTER;USERS_LDAP_GROUP_FILTER" desc:"LDAP filter to add to the default filters for group searches." introductionVersion:"1.0.0"`
UserObjectClass string `yaml:"user_object_class" env:"OC_LDAP_USER_OBJECTCLASS;USERS_LDAP_USER_OBJECTCLASS" desc:"The object class to use for users in the default user search filter like 'inetOrgPerson'." introductionVersion:"1.0.0"`
GroupObjectClass string `yaml:"group_object_class" env:"OC_LDAP_GROUP_OBJECTCLASS;USERS_LDAP_GROUP_OBJECTCLASS" desc:"The object class to use for groups in the default group search filter like 'groupOfNames'." introductionVersion:"1.0.0"`
IDP string `yaml:"idp" env:"OC_URL;OC_OIDC_ISSUER;USERS_IDP_URL" desc:"The identity provider value to set in the userids of the CS3 user objects for users returned by this user provider." introductionVersion:"1.0.0"`
DisableUserMechanism string `yaml:"disable_user_mechanism" env:"OC_LDAP_DISABLE_USER_MECHANISM;USERS_LDAP_DISABLE_USER_MECHANISM" desc:"An option to control the behavior for disabling users. Valid options are 'none', 'attribute' and 'group'. If set to 'group', disabling a user via API will add the user to the configured group for disabled users, if set to 'attribute' this will be done in the ldap user entry, if set to 'none' the disable request is not processed." introductionVersion:"1.0.0"`
UserTypeAttribute string `yaml:"user_type_attribute" env:"OC_LDAP_USER_SCHEMA_USER_TYPE;USERS_LDAP_USER_TYPE_ATTRIBUTE" desc:"LDAP Attribute to distinguish between 'Member' and 'Guest' users. Default is 'openCloudUserType'." introductionVersion:"1.0.0"`
LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"OC_LDAP_DISABLED_USERS_GROUP_DN;USERS_LDAP_DISABLED_USERS_GROUP_DN" desc:"The distinguished name of the group to which added users will be classified as disabled when 'disable_user_mechanism' is set to 'group'." introductionVersion:"1.0.0"`
UserSchema LDAPUserSchema `yaml:"user_schema"`
GroupSchema LDAPGroupSchema `yaml:"group_schema"`
LookupCacheTTL time.Duration `yaml:"lookup_cache_ttl" env:"OC_LDAP_LOOKUP_CACHE_TTL;USERS_LDAP_LOOKUP_CACHE_TTL" desc:"The time to live for the in-memory cache of LDAP lookups. This cache is used to reduce the number of LDAP queries for user and group lookups." introductionVersion:"%%NEXT%%"`
}
type LDAPUserSchema struct {
@@ -1,6 +1,8 @@
package defaults
import (
"time"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/users/pkg/config"
@@ -66,6 +68,7 @@ func DefaultConfig() *config.Config {
Groupname: "cn",
Member: "member",
},
LookupCacheTTL: 10 * time.Second,
},
JSON: config.JSONDriver{},
OwnCloudSQL: config.OwnCloudSQLDriver{
+1
View File
@@ -103,5 +103,6 @@ func ldapConfigFromString(cfg config.LDAPDriver) map[string]any {
"externalId": cfg.TenantSchema.ExternalID,
"name": cfg.TenantSchema.Name,
},
"lookup_cache_ttl": cfg.LookupCacheTTL,
}
}
+338
View File
@@ -0,0 +1,338 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package expirable
import (
"sync"
"time"
"github.com/hashicorp/golang-lru/v2/internal"
)
// EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback[K comparable, V any] func(key K, value V)
// LRU implements a thread-safe LRU with expirable entries.
type LRU[K comparable, V any] struct {
size int
evictList *internal.LruList[K, V]
items map[K]*internal.Entry[K, V]
onEvict EvictCallback[K, V]
// expirable options
mu sync.Mutex
ttl time.Duration
done chan struct{}
// buckets for expiration
buckets []bucket[K, V]
// uint8 because it's number between 0 and numBuckets
nextCleanupBucket uint8
}
// bucket is a container for holding entries to be expired
type bucket[K comparable, V any] struct {
entries map[K]*internal.Entry[K, V]
newestEntry time.Time
}
// noEvictionTTL - very long ttl to prevent eviction
const noEvictionTTL = time.Hour * 24 * 365 * 10
// because of uint8 usage for nextCleanupBucket, should not exceed 256.
// casting it as uint8 explicitly requires type conversions in multiple places
const numBuckets = 100
// NewLRU returns a new thread-safe cache with expirable entries.
//
// Size parameter set to 0 makes cache of unlimited size, e.g. turns LRU mechanism off.
//
// Providing 0 TTL turns expiring off.
//
// Delete expired entries every 1/100th of ttl value. Goroutine which deletes expired entries runs indefinitely.
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V], ttl time.Duration) *LRU[K, V] {
if size < 0 {
size = 0
}
if ttl <= 0 {
ttl = noEvictionTTL
}
res := LRU[K, V]{
ttl: ttl,
size: size,
evictList: internal.NewList[K, V](),
items: make(map[K]*internal.Entry[K, V]),
onEvict: onEvict,
done: make(chan struct{}),
}
// initialize the buckets
res.buckets = make([]bucket[K, V], numBuckets)
for i := 0; i < numBuckets; i++ {
res.buckets[i] = bucket[K, V]{entries: make(map[K]*internal.Entry[K, V])}
}
// enable deleteExpired() running in separate goroutine for cache with non-zero TTL
//
// Important: done channel is never closed, so deleteExpired() goroutine will never exit,
// it's decided to add functionality to close it in the version later than v2.
if res.ttl != noEvictionTTL {
go func(done <-chan struct{}) {
ticker := time.NewTicker(res.ttl / numBuckets)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
res.deleteExpired()
}
}
}(res.done)
}
return &res
}
// Purge clears the cache completely.
// onEvict is called for each evicted key.
func (c *LRU[K, V]) Purge() {
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range c.items {
if c.onEvict != nil {
c.onEvict(k, v.Value)
}
delete(c.items, k)
}
for _, b := range c.buckets {
for _, ent := range b.entries {
delete(b.entries, ent.Key)
}
}
c.evictList.Init()
}
// Add adds a value to the cache. Returns true if an eviction occurred.
// Returns false if there was no eviction: the item was already in the cache,
// or the size was not exceeded.
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
c.removeFromBucket(ent) // remove the entry from its current bucket as expiresAt is renewed
ent.Value = value
ent.ExpiresAt = now.Add(c.ttl)
c.addToBucket(ent)
return false
}
// Add new item
ent := c.evictList.PushFrontExpirable(key, value, now.Add(c.ttl))
c.items[key] = ent
c.addToBucket(ent) // adds the entry to the appropriate bucket and sets entry.expireBucket
evict := c.size > 0 && c.evictList.Length() > c.size
// Verify size not exceeded
if evict {
c.removeOldest()
}
return evict
}
// Get looks up a key's value from the cache.
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
var ent *internal.Entry[K, V]
if ent, ok = c.items[key]; ok {
// Expired item check
if time.Now().After(ent.ExpiresAt) {
return value, false
}
c.evictList.MoveToFront(ent)
return ent.Value, true
}
return
}
// Contains checks if a key is in the cache, without updating the recent-ness
// or deleting it for being stale.
func (c *LRU[K, V]) Contains(key K) (ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
_, ok = c.items[key]
return ok
}
// Peek returns the key value (or undefined if not found) without updating
// the "recently used"-ness of the key.
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
var ent *internal.Entry[K, V]
if ent, ok = c.items[key]; ok {
// Expired item check
if time.Now().After(ent.ExpiresAt) {
return value, false
}
return ent.Value, true
}
return
}
// Remove removes the provided key from the cache, returning if the
// key was contained.
func (c *LRU[K, V]) Remove(key K) bool {
c.mu.Lock()
defer c.mu.Unlock()
if ent, ok := c.items[key]; ok {
c.removeElement(ent)
return true
}
return false
}
// RemoveOldest removes the oldest item from the cache.
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
return ent.Key, ent.Value, true
}
return
}
// GetOldest returns the oldest entry
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if ent := c.evictList.Back(); ent != nil {
return ent.Key, ent.Value, true
}
return
}
// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *LRU[K, V]) Keys() []K {
c.mu.Lock()
defer c.mu.Unlock()
keys := make([]K, 0, len(c.items))
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
keys = append(keys, ent.Key)
}
return keys
}
// Values returns a slice of the values in the cache, from oldest to newest.
// Expired entries are filtered out.
func (c *LRU[K, V]) Values() []V {
c.mu.Lock()
defer c.mu.Unlock()
values := make([]V, len(c.items))
i := 0
now := time.Now()
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
if now.After(ent.ExpiresAt) {
continue
}
values[i] = ent.Value
i++
}
return values
}
// Len returns the number of items in the cache.
func (c *LRU[K, V]) Len() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.evictList.Length()
}
// Resize changes the cache size. Size of 0 means unlimited.
func (c *LRU[K, V]) Resize(size int) (evicted int) {
c.mu.Lock()
defer c.mu.Unlock()
if size <= 0 {
c.size = 0
return 0
}
diff := c.evictList.Length() - size
if diff < 0 {
diff = 0
}
for i := 0; i < diff; i++ {
c.removeOldest()
}
c.size = size
return diff
}
// Close destroys cleanup goroutine. To clean up the cache, run Purge() before Close().
// func (c *LRU[K, V]) Close() {
// c.mu.Lock()
// defer c.mu.Unlock()
// select {
// case <-c.done:
// return
// default:
// }
// close(c.done)
// }
// removeOldest removes the oldest item from the cache. Has to be called with lock!
func (c *LRU[K, V]) removeOldest() {
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
}
}
// removeElement is used to remove a given list element from the cache. Has to be called with lock!
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
c.evictList.Remove(e)
delete(c.items, e.Key)
c.removeFromBucket(e)
if c.onEvict != nil {
c.onEvict(e.Key, e.Value)
}
}
// deleteExpired deletes expired records from the oldest bucket, waiting for the newest entry
// in it to expire first.
func (c *LRU[K, V]) deleteExpired() {
c.mu.Lock()
bucketIdx := c.nextCleanupBucket
timeToExpire := time.Until(c.buckets[bucketIdx].newestEntry)
// wait for newest entry to expire before cleanup without holding lock
if timeToExpire > 0 {
c.mu.Unlock()
time.Sleep(timeToExpire)
c.mu.Lock()
}
for _, ent := range c.buckets[bucketIdx].entries {
c.removeElement(ent)
}
c.nextCleanupBucket = (c.nextCleanupBucket + 1) % numBuckets
c.mu.Unlock()
}
// addToBucket adds entry to expire bucket so that it will be cleaned up when the time comes. Has to be called with lock!
func (c *LRU[K, V]) addToBucket(e *internal.Entry[K, V]) {
bucketID := (numBuckets + c.nextCleanupBucket - 1) % numBuckets
e.ExpireBucket = bucketID
c.buckets[bucketID].entries[e.Key] = e
if c.buckets[bucketID].newestEntry.Before(e.ExpiresAt) {
c.buckets[bucketID].newestEntry = e.ExpiresAt
}
}
// removeFromBucket removes the entry from its corresponding bucket. Has to be called with lock!
func (c *LRU[K, V]) removeFromBucket(e *internal.Entry[K, V]) {
delete(c.buckets[e.ExpireBucket].entries, e.Key)
}
+14
View File
@@ -53,6 +53,20 @@ func (re *Event) Ack() error {
return re.msg.Ack()
}
func (re *Event) Nak() error {
if re.msg == nil {
return errors.New("cannot nack event without message")
}
return re.msg.Nak()
}
func (re *Event) Term() error {
if re.msg == nil {
return errors.New("cannot terminate event without message")
}
return re.msg.Term()
}
func (re *Event) InProgress() error {
if re.msg == nil {
return errors.New("cannot mark event as in progress without message")
@@ -82,6 +82,7 @@ var (
events.PostprocessingFinished{},
events.PostprocessingStepFinished{},
events.RestartPostprocessing{},
events.StartPostprocessingStep{},
}
)
@@ -441,6 +442,22 @@ func (fs *Decomposedfs) Postprocessing(ch <-chan events.Event) {
}); err != nil {
sublog.Error().Err(err).Msg("Failed to publish BytesReceived event")
}
case events.StartPostprocessingStep:
sublog := log.With().Str("event", "StartPostprocessingStep").Str("uploadid", ev.UploadID).Logger()
if ev.UploadID == "" {
sublog.Error().Msg("UploadID is empty, cannot start postprocessing step")
continue
}
session, err := fs.sessionStore.Get(ctx, ev.UploadID)
if err != nil {
sublog.Error().Err(err).Msg("Failed to get upload")
continue
}
session.SetStatus(upload.SessionStatusProcessing, "started postprocessing step: "+string(ev.StepToStart))
err = session.Persist(ctx)
if err != nil {
sublog.Error().Err(err).Msg("Failed to persist upload session after starting postprocessing step")
}
case events.PostprocessingStepFinished:
sublog := log.With().Str("event", "PostprocessingStepFinished").Str("uploadid", ev.UploadID).Logger()
if ev.FinishedStep != events.PPStepAntivirus {
@@ -552,6 +569,14 @@ func (fs *Decomposedfs) Postprocessing(ch <-chan events.Event) {
sublog = log.With().Str("spaceid", session.SpaceID()).Str("nodeid", session.NodeID()).Logger()
session.SetScanData(res.Description, res.Scandate)
var status string
switch ev.Outcome {
case events.PPOutcomeAbort, events.PPOutcomeDelete:
status = upload.SessionStatusFailed
default:
status = upload.SessionStatusProcessing
}
session.SetStatus(status, "finished postprocessing step: "+string(ev.FinishedStep))
if err := session.Persist(ctx); err != nil {
sublog.Error().Err(err).Msg("Failed to persist scan results")
}
@@ -102,6 +102,7 @@ const (
SpaceImageAttr string = OcPrefix + "space.image"
SpaceAliasAttr string = OcPrefix + "space.alias"
SpaceTenantIDAttr string = OcPrefix + "space.tenantid"
SpaceContentTypeAttr string = OcPrefix + "space.contenttype"
UserAcePrefix string = "u:"
GroupAcePrefix string = "g:"
@@ -99,6 +99,7 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr
}
description := utils.ReadPlainFromOpaque(req.Opaque, "description")
contentType := utils.ReadPlainFromOpaque(req.Opaque, "contentType")
alias := utils.ReadPlainFromOpaque(req.Opaque, "spaceAlias")
if alias == "" {
alias = templates.WithSpacePropertiesAndUser(u, req.Type, req.Name, spaceID, fs.o.GeneralSpaceAliasTemplate)
@@ -192,6 +193,10 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr
metadata.SetString(prefixes.SpaceDescriptionAttr, description)
}
if contentType != "" {
metadata.SetString(prefixes.SpaceContentTypeAttr, contentType)
}
if alias != "" {
metadata.SetString(prefixes.SpaceAliasAttr, alias)
}
@@ -607,6 +612,9 @@ func (fs *Decomposedfs) UpdateStorageSpace(ctx context.Context, req *provider.Up
if description, ok := space.Opaque.Map["description"]; ok {
metadata[prefixes.SpaceDescriptionAttr] = description.Value
}
if contentType, ok := space.Opaque.Map["contentType"]; ok {
metadata[prefixes.SpaceContentTypeAttr] = contentType.Value
}
if alias := utils.ReadPlainFromOpaque(space.Opaque, "spaceAlias"); alias != "" {
metadata.SetString(prefixes.SpaceAliasAttr, alias)
}
@@ -660,7 +668,7 @@ func (fs *Decomposedfs) UpdateStorageSpace(ctx context.Context, req *provider.Up
if !permissions.IsManager(sp) {
// We are not a space manager. We need to check for additional permissions.
k := []string{prefixes.NameAttr, prefixes.SpaceDescriptionAttr}
k := []string{prefixes.NameAttr, prefixes.SpaceDescriptionAttr, prefixes.SpaceContentTypeAttr}
if !permissions.IsEditor(sp) {
k = append(k, prefixes.SpaceReadmeAttr, prefixes.SpaceAliasAttr, prefixes.SpaceImageAttr)
}
@@ -1118,6 +1126,9 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node,
if sd := spaceAttributes.String(prefixes.SpaceDescriptionAttr); sd != "" {
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "description", sd)
}
if se := spaceAttributes.String(prefixes.SpaceContentTypeAttr); se != "" {
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "contentType", se)
}
if sr := spaceAttributes.String(prefixes.SpaceReadmeAttr); sr != "" {
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "readme", storagespace.FormatResourceID(
&provider.ResourceId{StorageId: space.Root.StorageId, SpaceId: space.GetRoot().GetSpaceId(), OpaqueId: sr},
@@ -46,6 +46,12 @@ type DecomposedFsSession struct {
info tusd.FileInfo
}
const (
SessionStatusUploading = "uploading"
SessionStatusProcessing = "processing"
SessionStatusFailed = "failed"
)
// Context returns a context with the user, logger and lockid used when initiating the upload session
func (session *DecomposedFsSession) Context(ctx context.Context) context.Context { // restore logger from file info
sub := session.store.log.With().Int("pid", os.Getpid()).Logger()
@@ -307,10 +313,9 @@ func (session *DecomposedFsSession) MTime() time.Time {
return t
}
// IsProcessing returns true if all bytes have been received. The session then has entered postprocessing state.
// IsProcessing returns true if the upload is in the processing state, meaning that the upload has finished and postprocessing is still running.
func (session *DecomposedFsSession) IsProcessing() bool {
// We might need a more sophisticated way to determine processing status soon
return session.info.Size == session.info.Offset && session.info.MetaData["scanResult"] == ""
return session.info.MetaData["status"] == SessionStatusProcessing
}
// binPath returns the path to the file storing the binary data.
@@ -339,6 +344,22 @@ func (session *DecomposedFsSession) ScanData() (string, time.Time) {
return session.info.MetaData["scanResult"], d
}
// SetStatus sets the status of the upload session
func (session *DecomposedFsSession) SetStatus(status, msg string) {
session.info.MetaData["status"] = status
session.info.MetaData["statusMessage"] = msg
}
// Status returns the status of the upload session
func (session *DecomposedFsSession) Status() string {
return session.info.MetaData["status"]
}
// StatusMessage returns the status message of the upload session
func (session *DecomposedFsSession) StatusMessage() string {
return session.info.MetaData["statusMessage"]
}
// sessionPath returns the path to the .info file storing the file's info.
func sessionPath(root, id string) string {
return filepath.Join(root, "uploads", id+".info")
@@ -94,7 +94,9 @@ func (store DecomposedFsStore) New(ctx context.Context) *DecomposedFsSession {
Storage: map[string]string{
"Type": "DecomposedFsStore",
},
MetaData: tusd.MetaData{},
MetaData: tusd.MetaData{
"status": string(SessionStatusUploading),
},
},
}
}
@@ -194,8 +194,18 @@ func (session *DecomposedFsSession) FinishUploadDecomposed(ctx context.Context)
n, err := session.store.CreateNodeForUpload(ctx, session, attrs)
if err != nil {
session.SetStatus(SessionStatusFailed, err.Error())
if perr := session.Persist(ctx); perr != nil {
log.Error().Err(perr).Msg("failed to persist upload session after setting status to failed")
}
return err
}
session.SetStatus(SessionStatusProcessing, "")
if err = session.Persist(ctx); err != nil {
log.Error().Err(err).Msg("failed to persist upload session after setting status to processing")
}
// increase the processing counter for every started processing
// will be decreased in Cleanup()
metrics.UploadProcessing.Inc()
@@ -493,12 +503,18 @@ func (session *DecomposedFsSession) Cleanup(revertNodeMetadata, cleanBin, cleanI
// URL returns a url to download an upload
func (session *DecomposedFsSession) URL(_ context.Context) (string, error) {
u := joinurl(session.store.tknopts.DownloadEndpoint, "tus/", session.ID())
if session.store.tknopts.DataGatewayEndpoint == "" {
return u, nil
}
// we need to create a token
type transferClaims struct {
jwt.RegisteredClaims
Target string `json:"target"`
}
u := joinurl(session.store.tknopts.DownloadEndpoint, "tus/", session.ID())
ttl := time.Duration(session.store.tknopts.TransferExpires) * time.Second
claims := transferClaims{
RegisteredClaims: jwt.RegisteredClaims{
+5
View File
@@ -94,6 +94,11 @@ type UploadSession interface {
// ScanData returns the scan data for the UploadSession
ScanData() (string, time.Time)
// Status returns the status for the UploadSession
Status() string
// StatusMessage returns the status message for the UploadSession
StatusMessage() string
}
// UploadSessionFilter can be used to filter upload sessions
@@ -338,6 +338,22 @@ func (s *OcisSession) ScanData() (string, time.Time) {
return s.info.MetaData["scanResult"], d
}
func (s *OcisSession) SetStatus(status string) {
s.info.MetaData["status"] = status
}
func (s *OcisSession) Status() string {
return s.info.MetaData["status"]
}
func (s *OcisSession) SetStatusMessage(message string) {
s.info.MetaData["status_message"] = message
}
func (s *OcisSession) StatusMessage() string {
return s.info.MetaData["status_message"]
}
// sessionPath returns the path to the .info file storing the file's info.
func sessionPath(root, id string) string {
return filepath.Join(root, "uploads", id+".info")
@@ -371,12 +371,17 @@ func (session *OcisSession) Cleanup(revertNodeMetadata, cleanBin, cleanInfo bool
// URL returns a url to download an upload
func (session *OcisSession) URL(_ context.Context) (string, error) {
u := joinurl(session.store.tknopts.DownloadEndpoint, "tus/", session.ID())
if session.store.tknopts.DataGatewayEndpoint == "" {
return u, nil
}
// we need to create a token
type transferClaims struct {
jwt.RegisteredClaims
Target string `json:"target"`
}
u := joinurl(session.store.tknopts.DownloadEndpoint, "tus/", session.ID())
ttl := time.Duration(session.store.tknopts.TransferExpires) * time.Second
claims := transferClaims{
RegisteredClaims: jwt.RegisteredClaims{
+73 -4
View File
@@ -23,10 +23,12 @@ import (
"errors"
"fmt"
"strings"
"time"
identityUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
@@ -38,12 +40,44 @@ import (
// Identity provides methods to query users and groups from an LDAP server
type Identity struct {
User userConfig `mapstructure:",squash"`
Group groupConfig `mapstructure:",squash"`
Tenant tenantConfig `mapstructure:",squash"`
User userConfig `mapstructure:",squash"`
Group groupConfig `mapstructure:",squash"`
Tenant tenantConfig `mapstructure:",squash"`
LookupCacheTTL time.Duration `mapstructure:"lookup_cache_ttl"`
lookupCache entryCache
}
const tracerName = "pkg/utils/ldap"
// entryCache is a small nil-safe wrapper around the expirable LRU used to cache
// LDAP lookups. A zero-value entryCache (nil lru) is a valid, disabled cache:
// Get always misses and Add is a no-op. This lets the cache be turned off
// entirely (e.g. in acceptance tests) by configuring a lookup_cache_ttl of 0.
type entryCache struct {
lru *expirable.LRU[string, *ldap.Entry]
}
// Get returns the cached entry for key. It reports a miss when the cache is
// disabled (nil lru).
func (c entryCache) Get(key string) (*ldap.Entry, bool) {
if c.lru == nil {
return nil, false
}
return c.lru.Get(key)
}
// Add stores entry under key. It is a no-op when the cache is disabled (nil lru).
func (c entryCache) Add(key string, entry *ldap.Entry) {
if c.lru == nil {
return
}
c.lru.Add(key, entry)
}
const (
tracerName = "pkg/utils/ldap"
lookupCacheSize = 1024
)
type userConfig struct {
BaseDN string `mapstructure:"user_base_dn"`
@@ -223,6 +257,14 @@ func (i *Identity) Setup() error {
}
}
if i.LookupCacheTTL > 0 {
i.lookupCache = entryCache{lru: expirable.NewLRU[string, *ldap.Entry](lookupCacheSize, nil, i.LookupCacheTTL)}
} else {
// A TTL of 0 disables the lookup cache entirely. A zero-value
// entryCache always misses and never stores anything.
i.lookupCache = entryCache{}
}
return nil
}
@@ -254,6 +296,12 @@ func (i *Identity) GetLDAPUserByFilter(ctx context.Context, lc ldap.Client, filt
log := appctx.GetLogger(ctx)
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPUserByFilter")
defer span.End()
cacheKey := fmt.Sprintf("user:filter:%s", filter)
if cached, ok := i.lookupCache.Get(cacheKey); ok {
return cached, nil
}
searchRequest := ldap.NewSearchRequest(
i.User.BaseDN, i.User.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
@@ -275,6 +323,7 @@ func (i *Identity) GetLDAPUserByFilter(ctx context.Context, lc ldap.Client, filt
return nil, errtypes.NotFound(filter)
}
span.SetStatus(codes.Ok, "")
i.lookupCache.Add(cacheKey, res.Entries[0])
return res.Entries[0], nil
}
@@ -286,6 +335,11 @@ func (i *Identity) GetLDAPUserByDN(ctx context.Context, lc ldap.Client, dn strin
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPUserByDN")
defer span.End()
cacheKey := fmt.Sprintf("user:dn:%s", dn)
if cached, ok := i.lookupCache.Get(cacheKey); ok {
return cached, nil
}
filter := fmt.Sprintf("(objectclass=%s)", i.User.Objectclass)
if i.User.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", i.User.Filter, filter)
@@ -310,6 +364,7 @@ func (i *Identity) GetLDAPUserByDN(ctx context.Context, lc ldap.Client, dn strin
if len(res.Entries) == 0 {
return nil, errtypes.NotFound(dn)
}
i.lookupCache.Add(cacheKey, res.Entries[0])
return res.Entries[0], nil
}
@@ -483,6 +538,12 @@ func (i *Identity) GetLDAPGroupByFilter(ctx context.Context, lc ldap.Client, fil
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPGroupByFilter")
defer span.End()
log := appctx.GetLogger(ctx)
cacheKey := fmt.Sprintf("group:filter:%s", filter)
if cached, ok := i.lookupCache.Get(cacheKey); ok {
return cached, nil
}
searchRequest := ldap.NewSearchRequest(
i.Group.BaseDN, i.Group.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
@@ -511,6 +572,7 @@ func (i *Identity) GetLDAPGroupByFilter(ctx context.Context, lc ldap.Client, fil
return nil, errtypes.NotFound(filter)
}
span.SetStatus(codes.Ok, "")
i.lookupCache.Add(cacheKey, res.Entries[0])
return res.Entries[0], nil
}
@@ -901,6 +963,12 @@ func (i *Identity) GetLDAPTenantByFilter(ctx context.Context, lc ldap.Client, fi
log := appctx.GetLogger(ctx)
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPTenantByFilter")
defer span.End()
cacheKey := fmt.Sprintf("tenant:filter:%s", filter)
if cached, ok := i.lookupCache.Get(cacheKey); ok {
return cached, nil
}
searchRequest := ldap.NewSearchRequest(
i.Tenant.BaseDN, i.Tenant.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
@@ -922,6 +990,7 @@ func (i *Identity) GetLDAPTenantByFilter(ctx context.Context, lc ldap.Client, fi
return nil, errtypes.NotFound(filter)
}
span.SetStatus(codes.Ok, "")
i.lookupCache.Add(cacheKey, res.Entries[0])
return res.Entries[0], nil
}
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
import (
"context"
@@ -8,4 +8,4 @@ Use [NewClientHandler] with [grpc.WithStatsHandler] to instrument a gRPC client.
Use [NewServerHandler] with [grpc.StatsHandler] to instrument a gRPC server.
*/
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
@@ -1,21 +1,62 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
// gRPC tracing middleware
// https://opentelemetry.io/docs/specs/semconv/rpc/
import (
"net"
"net/url"
"strconv"
"strings"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
grpc_codes "google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// serverAddrAttrsFromCanonicalTarget extracts server address attributes from a
// canonical gRPC target as returned by [google.golang.org/grpc.ClientConn.CanonicalTarget].
// Canonical targets always have the form "scheme://[authority]/endpoint", e.g.:
//
// passthrough:///127.0.0.1:7777 → ServerAddress("127.0.0.1"), ServerPort(7777)
// dns:///example.com:443 → ServerAddress("example.com"), ServerPort(443)
// dns://authority/example.com:443 → ServerAddress("example.com"), ServerPort(443)
// unix:///tmp/grpc.sock → ServerAddress("/tmp/grpc.sock")
func serverAddrAttrsFromCanonicalTarget(target string) []attribute.KeyValue {
// Fast path: confirmed host:port with numeric port, no scheme involved.
if h, pStr, err := net.SplitHostPort(target); err == nil {
if p, err := strconv.Atoi(pStr); err == nil {
return []attribute.KeyValue{
semconv.ServerAddress(h),
semconv.ServerPort(p),
}
}
}
// gRPC URI: scheme://authority/endpoint → endpoint in Path
// scheme:endpoint → endpoint in Opaque
u, err := url.Parse(target)
if err != nil {
return []attribute.KeyValue{semconv.ServerAddress(target)}
}
ep := u.Path
if u.Scheme != "unix" && u.Scheme != "unix-abstract" {
// Strip the leading "/" added by url.Parse for hierarchical URIs;
// preserve it for unix socket paths where the slash is meaningful.
ep = strings.TrimPrefix(ep, "/")
}
if ep == "" {
ep = u.Opaque
}
if ep != "" {
return serverAddrAttrs(ep)
}
return []attribute.KeyValue{semconv.ServerAddress(target)}
}
// serverAddrAttrs returns the server address attributes for the hostport.
func serverAddrAttrs(hostport string) []attribute.KeyValue {
h, pStr, err := net.SplitHostPort(hostport)
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
import (
"google.golang.org/grpc"
@@ -2,14 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
// Package internal provides internal functionality for the otelgrpc package.
package internal // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal"
package internal
import (
"strings"
"go.opentelemetry.io/otel/attribute"
oldsemconv "go.opentelemetry.io/otel/semconv/v1.37.0" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
)
// ParseFullMethod returns a span name following the OpenTelemetry semantic
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
import (
"context"
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
import (
"context"
@@ -13,8 +13,8 @@ import (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
oldrpcconv "go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/semconv/v1.41.0/rpcconv"
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
"go.opentelemetry.io/otel/semconv/v1.43.0/rpcconv"
"go.opentelemetry.io/otel/trace"
grpc_codes "google.golang.org/grpc/codes"
@@ -26,6 +26,11 @@ import (
type gRPCContextKey struct{}
// dialTargetContextKey carries the dial target seeded by client interceptors
// so the stats handler can use the hostname for server.address instead of the
// post-DNS IP from RemoteAddr.
type dialTargetContextKey struct{}
type gRPCContext struct {
metricAttrs []attribute.KeyValue
record bool
@@ -328,6 +333,20 @@ func (*config) handleRPC(
switch rs := rs.(type) {
case *stats.Begin:
// Set server.address early from the dial target when available (upcoming
// interceptors will seed this). Covers both success and failure paths since Begin
// fires on every RPC regardless of outcome.
if rs.Client {
if target, ok := ctx.Value(dialTargetContextKey{}).(string); ok && target != "" {
attrs := serverAddrAttrsFromCanonicalTarget(target)
if span.IsRecording() {
span.SetAttributes(attrs...)
}
if gctx != nil {
gctx.metricAttrs = append(gctx.metricAttrs, attrs...)
}
}
}
case *stats.InPayload:
case *stats.InHeader:
if !rs.Client && rs.LocalAddr != nil {
@@ -339,13 +358,18 @@ func (*config) handleRPC(
case *stats.OutPayload:
case *stats.OutTrailer:
case *stats.OutHeader:
// Only use the resolved IP from RemoteAddr when no dial target was seeded
// (i.e. NewClientHandler callers without interceptors). When dialTargetContextKey
// is present, Begin already set server.address to the hostname.
if rs.Client && rs.RemoteAddr != nil && (span.IsRecording() || gctx != nil) {
attrs := serverAddrAttrs(rs.RemoteAddr.String())
if span.IsRecording() {
span.SetAttributes(attrs...)
}
if gctx != nil {
gctx.metricAttrs = append(gctx.metricAttrs, attrs...)
if target, ok := ctx.Value(dialTargetContextKey{}).(string); !ok || target == "" {
attrs := serverAddrAttrs(rs.RemoteAddr.String())
if span.IsRecording() {
span.SetAttributes(attrs...)
}
if gctx != nil {
gctx.metricAttrs = append(gctx.metricAttrs, attrs...)
}
}
}
case *stats.End:
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
// Version is the current release version of the gRPC instrumentation.
const Version = "0.69.0"
const Version = "0.70.0"
-17
View File
@@ -1,17 +0,0 @@
<!-- Generated. DO NOT MODIFY. -->
# Migration from v1.40.0 to v1.41.0
The `go.opentelemetry.io/otel/semconv/v1.41.0` package should be a drop-in replacement for `go.opentelemetry.io/otel/semconv/v1.40.0` with the following exceptions.
## Removed
The following declarations have been removed.
Refer to the [OpenTelemetry Semantic Conventions documentation] for deprecation instructions.
If the type is not listed in the documentation as deprecated, it has been removed in this version due to lack of applicability or use.
If you use any of these non-deprecated declarations in your Go application, please [open an issue] describing your use-case.
- `DeploymentEnvironmentName`
[OpenTelemetry Semantic Conventions documentation]: https://github.com/open-telemetry/semantic-conventions
[open an issue]: https://github.com/open-telemetry/opentelemetry-go/issues/new?template=Blank+issue
-3
View File
@@ -1,3 +0,0 @@
# Semconv v1.41.0
[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/semconv/v1.41.0)](https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.41.0)
File diff suppressed because it is too large. Load diff
-11
View File
@@ -1,11 +0,0 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package semconv implements OpenTelemetry semantic conventions.
//
// OpenTelemetry semantic conventions are agreed standardized naming
// patterns for OpenTelemetry things. This package represents the v1.41.0
// version of the OpenTelemetry semantic conventions.
package semconv
-83
View File
@@ -1,83 +0,0 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package semconv
import (
"errors"
"fmt"
"reflect"
"go.opentelemetry.io/otel/attribute"
)
// ErrorType returns an [attribute.KeyValue] identifying the error type of err.
//
// If err is nil, the returned attribute has the default value
// [ErrorTypeOther].
//
// If err or one of the errors in its chain has the method
//
// ErrorType() string
//
// the returned attribute has that method's return value. If multiple errors in
// the chain implement this method, the value from the first match found by
// [errors.As] is used. Otherwise, the returned attribute has a value derived
// from the concrete type of err after unwrapping any wrappers created with
// [fmt.Errorf].
//
// The key of the returned attribute is [ErrorTypeKey].
func ErrorType(err error) attribute.KeyValue {
if err == nil {
return ErrorTypeOther
}
return ErrorTypeKey.String(errorType(err))
}
func errorType(err error) string {
var s string
if et, ok := err.(interface{ ErrorType() string }); ok {
// Fast path: check the top-level error first.
s = et.ErrorType()
} else {
// Fallback: search the error chain for an ErrorType method.
var et interface{ ErrorType() string }
if errors.As(err, &et) {
// Prioritize the ErrorType method if available.
s = et.ErrorType()
}
}
if s == "" {
// Fallback to reflection if the ErrorType method is not supported or
// returns an empty value.
t := reflect.TypeOf(unwrapFmtWrapped(err))
pkg, name := t.PkgPath(), t.Name()
if pkg != "" && name != "" {
s = pkg + "." + name
} else {
// The type has no package path or name (predeclared, not-defined,
// or alias for a not-defined type).
//
// This is not guaranteed to be unique, but is a best effort.
s = t.String()
}
}
return s
}
var fmtWrapErrorType = reflect.TypeOf(fmt.Errorf("wrapped: %w", errors.New("err")))
func unwrapFmtWrapped(err error) error {
for reflect.TypeOf(err) == fmtWrapErrorType {
u := errors.Unwrap(err)
if u == nil {
return err // When the wrapped error is nil, use the concrete type of the wrapper.
}
err = u
}
return err
}
-11
View File
@@ -1,11 +0,0 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package semconv
const (
// ExceptionEventName is the name of the Span event representing an exception.
ExceptionEventName = "exception"
)
-11
View File
@@ -1,11 +0,0 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package semconv
// SchemaURL is the schema URL that matches the version of the semantic conventions
// that this package defines. Semconv packages starting from v1.4.0 must declare
// non-empty schema URL in the form https://opentelemetry.io/schemas/<version>
const SchemaURL = "https://opentelemetry.io/schemas/1.41.0"
+4 -4
View File
@@ -796,6 +796,7 @@ github.com/hashicorp/go-plugin/runner
# github.com/hashicorp/golang-lru/v2 v2.0.7
## explicit; go 1.18
github.com/hashicorp/golang-lru/v2
github.com/hashicorp/golang-lru/v2/expirable
github.com/hashicorp/golang-lru/v2/internal
github.com/hashicorp/golang-lru/v2/simplelru
# github.com/hashicorp/yamux v0.1.2
@@ -1364,7 +1365,7 @@ github.com/opencloud-eu/icap-client
# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55
## explicit; go 1.23
github.com/opencloud-eu/libre-graph-api-go
# github.com/opencloud-eu/reva/v2 v2.48.1-0.20260811071808-9bf68d2ccd96
# github.com/opencloud-eu/reva/v2 v2.48.1-0.20260819125853-c9416ac8b8c0
## explicit; go 1.25.8
github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace
github.com/opencloud-eu/reva/v2/cmd/revad/runtime
@@ -2302,7 +2303,7 @@ go.opencensus.io/trace/tracestate
## explicit; go 1.24.0
go.opentelemetry.io/auto/sdk
go.opentelemetry.io/auto/sdk/internal/telemetry
# go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0
# go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0
## explicit; go 1.25.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal
@@ -2333,11 +2334,10 @@ go.opentelemetry.io/otel/semconv/v1.20.0
go.opentelemetry.io/otel/semconv/v1.20.0/httpconv
go.opentelemetry.io/otel/semconv/v1.37.0
go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv
go.opentelemetry.io/otel/semconv/v1.41.0
go.opentelemetry.io/otel/semconv/v1.41.0/rpcconv
go.opentelemetry.io/otel/semconv/v1.43.0
go.opentelemetry.io/otel/semconv/v1.43.0/httpconv
go.opentelemetry.io/otel/semconv/v1.43.0/otelconv
go.opentelemetry.io/otel/semconv/v1.43.0/rpcconv
# go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0
## explicit; go 1.25.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace