diff --git a/go.work b/go.work index 65aefd7910..f8ccbe0459 100644 --- a/go.work +++ b/go.work @@ -3,8 +3,8 @@ go 1.21 use ( ./examples ./lib/fixtures - ./lib/ocrypto ./lib/flattening + ./lib/ocrypto ./protocol/go ./sdk ./service diff --git a/service/authorization/authorization.go b/service/authorization/authorization.go index a10009e4cc..69498acf77 100644 --- a/service/authorization/authorization.go +++ b/service/authorization/authorization.go @@ -49,9 +49,9 @@ type Config struct { type CustomRego struct { // Path to Rego file - Path string `mapstructure:"path"` + Path string `mapstructure:"path" json:"path"` // Rego Query - Query string `mapstructure:"query" default:"data.opentdf.entitlements.attributes"` + Query string `mapstructure:"query" json:"query" default:"data.opentdf.entitlements.attributes"` } func NewRegistration() serviceregistry.Registration { @@ -102,7 +102,7 @@ func NewRegistration() serviceregistry.Registration { } } - logger.Debug("authorization service config", slog.Any("config", authZCfg)) + logger.Debug("authorization service config", slog.Any("config", *authZCfg)) // Build Rego PreparedEvalQuery @@ -151,7 +151,7 @@ func (as AuthorizationService) IsReady(ctx context.Context) error { } func (as *AuthorizationService) GetDecisionsByToken(ctx context.Context, req *authorization.GetDecisionsByTokenRequest) (*authorization.GetDecisionsByTokenResponse, error) { - var decisionsRequests = []*authorization.DecisionRequest{} + decisionsRequests := []*authorization.DecisionRequest{} // for each token decision request for _, tdr := range req.GetDecisionRequests() { ecResp, err := as.sdk.EntityResoution.CreateEntityChainFromJwt(ctx, &entityresolution.CreateEntityChainFromJwtRequest{Tokens: tdr.GetTokens()}) @@ -171,7 +171,6 @@ func (as *AuthorizationService) GetDecisionsByToken(ctx context.Context, req *au resp, err := as.GetDecisions(ctx, &authorization.GetDecisionsRequest{ DecisionRequests: decisionsRequests, }) - if err != nil { return nil, err } @@ -264,7 +263,7 @@ func (as *AuthorizationService) GetDecisions(ctx context.Context, req *authoriza //nolint:nestif // handle empty entity / attr list if len(entities) == 0 || len(allPertinentFqnsRA.GetAttributeValueFqns()) == 0 { - as.logger.WarnContext(ctx, "Empty entity list and/or entity data attribute list") + as.logger.WarnContext(ctx, "empty entity list and/or entity data attribute list") } else { ecEntitlements, err := as.GetEntitlements(ctx, &req) if err != nil { @@ -304,7 +303,7 @@ func (as *AuthorizationService) GetDecisions(ctx context.Context, req *authoriza ) if err != nil { // TODO: should all decisions in a request fail if one entity entitlement lookup fails? - return nil, db.StatusifyError(err, db.ErrTextGetRetrievalFailed, slog.String("extra", "DetermineAccess request to Access PDP failed")) + return nil, db.StatusifyError(errors.New("could not determine access"), "could not determine access", slog.String("error", err.Error())) } // check the decisions decision := authorization.DecisionResponse_DECISION_PERMIT @@ -500,22 +499,22 @@ func (as *AuthorizationService) GetEntitlements(ctx context.Context, req *author // I am not sure how we would end up with multiple results but lets return an empty entitlement set for now if len(results) > 1 { - as.logger.WarnContext(ctx, "multiple entitlement results", slog.String("results", fmt.Sprintf("%+v", results))) + as.logger.WarnContext(ctx, "multiple entitlement results", slog.Any("results", results)) return rsp, nil } // If we get no expressions then we assume that the entity is not entitled to anything if len(results[0].Expressions) == 0 { - as.logger.WarnContext(ctx, "no entitlement expressions", slog.String("results", fmt.Sprintf("%+v", results))) + as.logger.WarnContext(ctx, "no entitlement expressions", slog.Any("results", results)) return rsp, nil } resultsEntitlements, entitlementsMapOk := results[0].Expressions[0].Value.(map[string]interface{}) if !entitlementsMapOk { - as.logger.ErrorContext(ctx, "entitlements is not a map[string]interface", slog.String("value", fmt.Sprintf("%+v", resultsEntitlements))) + as.logger.ErrorContext(ctx, "entitlements is not a map[string]interface", slog.Any("value", resultsEntitlements)) return rsp, nil } - as.logger.DebugContext(ctx, "opa results", "results", fmt.Sprintf("%+v", results)) + as.logger.DebugContext(ctx, "rego results", slog.Any("results", results)) for idx, entity := range req.GetEntities() { // Ensure the entity has an ID entityID := entity.GetId() @@ -525,14 +524,14 @@ func (as *AuthorizationService) GetEntitlements(ctx context.Context, req *author // Check to maksure if the value is a list. Good validation if someone customizes the rego policy entityEntitlements, valueListOk := resultsEntitlements[entityID].([]interface{}) if !valueListOk { - as.logger.ErrorContext(ctx, "entitlements is not a map[string]interface", slog.String("value", fmt.Sprintf("%+v", resultsEntitlements))) + as.logger.ErrorContext(ctx, "entitlements is not a map[string]interface", slog.Any("value", resultsEntitlements)) return rsp, nil } // map for attributes for optional comprehensive attributesMap := make(map[string]*policy.Attribute) // Build array with length of results - var entitlements = make([]string, len(entityEntitlements)) + entitlements := make([]string, len(entityEntitlements)) // Build entitlements list for valueIDX, value := range entityEntitlements { diff --git a/service/entityresolution/entityresolution.go b/service/entityresolution/entityresolution.go index ce49d64b2a..a539e57f63 100644 --- a/service/entityresolution/entityresolution.go +++ b/service/entityresolution/entityresolution.go @@ -2,9 +2,9 @@ package entityresolution import ( "context" - "encoding/json" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/mitchellh/mapstructure" "github.com/opentdf/platform/protocol/go/entityresolution" keycloak "github.com/opentdf/platform/service/entityresolution/keycloak" "github.com/opentdf/platform/service/logger" @@ -23,14 +23,13 @@ func NewRegistration() serviceregistry.Registration { ServiceDesc: &entityresolution.EntityResolutionService_ServiceDesc, RegisterFunc: func(srp serviceregistry.RegistrationParams) (any, serviceregistry.HandlerServer) { var inputIdpConfig keycloak.KeycloakConfig - confJSON, err := json.Marshal(srp.Config) - if err != nil { - panic(err) - } - err = json.Unmarshal(confJSON, &inputIdpConfig) - if err != nil { + + if err := mapstructure.Decode(srp.Config, &inputIdpConfig); err != nil { panic(err) } + + srp.Logger.Debug("entity_resolution configuration", "config", inputIdpConfig) + return &EntityResolutionService{idpConfig: inputIdpConfig, logger: srp.Logger}, func(ctx context.Context, mux *runtime.ServeMux, server any) error { return entityresolution.RegisterEntityResolutionServiceHandlerServer(ctx, mux, server.(entityresolution.EntityResolutionServiceServer)) //nolint:forcetypeassert // allow type assert, following other services } @@ -39,13 +38,11 @@ func NewRegistration() serviceregistry.Registration { } func (s EntityResolutionService) ResolveEntities(ctx context.Context, req *entityresolution.ResolveEntitiesRequest) (*entityresolution.ResolveEntitiesResponse, error) { - s.logger.Debug("request", "", req) resp, err := keycloak.EntityResolution(ctx, req, s.idpConfig, s.logger) return &resp, err } func (s EntityResolutionService) CreateEntityChainFromJwt(ctx context.Context, req *entityresolution.CreateEntityChainFromJwtRequest) (*entityresolution.CreateEntityChainFromJwtResponse, error) { - s.logger.Debug("request", "", req) resp, err := keycloak.CreateEntityChainFromJwt(ctx, req, s.idpConfig, s.logger) return &resp, err } diff --git a/service/entityresolution/keycloak/keycloak_entity_resolution.go b/service/entityresolution/keycloak/keycloak_entity_resolution.go index 60935fb3ec..ae271e9c95 100644 --- a/service/entityresolution/keycloak/keycloak_entity_resolution.go +++ b/service/entityresolution/keycloak/keycloak_entity_resolution.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "strings" "github.com/Nerzal/gocloak/v13" @@ -19,33 +20,49 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -const ErrTextCreationFailed = "resource creation failed" -const ErrTextGetRetrievalFailed = "resource retrieval failed" -const ErrTextNotFound = "resource not found" +const ( + ErrTextCreationFailed = "resource creation failed" + ErrTextGetRetrievalFailed = "resource retrieval failed" + ErrTextNotFound = "resource not found" +) -const ClientJwtSelector = "azp" -const UsernameJwtSelector = "preferred_username" +const ( + ClientJwtSelector = "azp" + UsernameJwtSelector = "preferred_username" +) const serviceAccountUsernamePrefix = "service-account-" type KeycloakConfig struct { - URL string `json:"url"` - Realm string `json:"realm"` - ClientID string `json:"clientid"` - ClientSecret string `json:"clientsecret"` - LegacyKeycloak bool `json:"legacykeycloak" default:"false"` - SubGroups bool `json:"subgroups" default:"false"` - InferID InferredIdentityConfig `json:"inferid,omitempty"` + URL string `mapstructure:"url" json:"url"` + Realm string `mapstructure:"realm" json:"realm"` + ClientID string `mapstructure:"clientid" json:"clientid"` + ClientSecret string `mapstructure:"clientsecret" json:"clientsecret"` + LegacyKeycloak bool `mapstructure:"legacykeycloak" json:"legacykeycloak" default:"false"` + SubGroups bool `mapstructure:"subgroups" json:"subgroups" default:"false"` + InferID InferredIdentityConfig `mapstructure:"inferid,omitempty" json:"inferid,omitempty"` +} + +func (c KeycloakConfig) LogValue() slog.Value { + return slog.GroupValue( + slog.String("url", c.URL), + slog.String("realm", c.Realm), + slog.String("clientid", c.ClientID), + slog.String("clientsecret", "[REDACTED]"), + slog.Bool("legacykeycloak", c.LegacyKeycloak), + slog.Bool("subgroups", c.SubGroups), + slog.Any("inferid", c.InferID), + ) } type InferredIdentityConfig struct { - From EntityImpliedFrom `json:"from,omitempty"` + From EntityImpliedFrom `mapstructure:"from,omitempty" json:"from,omitempty"` } type EntityImpliedFrom struct { - ClientID bool `json:"clientid,omitempty"` - Email bool `json:"email,omitempty"` - Username bool `json:"username,omitempty"` + ClientID bool `mapstructure:"clientid,omitempty" json:"clientid,omitempty"` + Email bool `mapstructure:"email,omitempty" json:"email,omitempty"` + Username bool `mapstructure:"username,omitempty" json:"username,omitempty"` } type KeyCloakConnector struct { @@ -59,7 +76,7 @@ func CreateEntityChainFromJwt( kcConfig KeycloakConfig, logger *logger.Logger, ) (entityresolution.CreateEntityChainFromJwtResponse, error) { - var entityChains = []*authorization.EntityChain{} + entityChains := []*authorization.EntityChain{} // for each token in the tokens form an entity chain for _, tok := range req.GetTokens() { entities, err := getEntitiesFromToken(ctx, kcConfig, tok.GetJwt(), logger) @@ -73,10 +90,8 @@ func CreateEntityChainFromJwt( } func EntityResolution(ctx context.Context, - req *entityresolution.ResolveEntitiesRequest, kcConfig KeycloakConfig, logger *logger.Logger) (entityresolution.ResolveEntitiesResponse, error) { - // note this only logs when run in test not when running in the OPE engine. - logger.Debug("EntityResolution", "req", fmt.Sprintf("%+v", req)) - // logger.Debug("EntityResolutionConfig", "config", fmt.Sprintf("%+v", kcConfig)) + req *entityresolution.ResolveEntitiesRequest, kcConfig KeycloakConfig, logger *logger.Logger, +) (entityresolution.ResolveEntitiesResponse, error) { connector, err := getKCClient(ctx, kcConfig, logger) if err != nil { return entityresolution.ResolveEntitiesResponse{}, @@ -85,22 +100,21 @@ func EntityResolution(ctx context.Context, payload := req.GetEntities() var resolvedEntities []*entityresolution.EntityRepresentation - logger.Debug("EntityResolution invoked", "payload", payload) for idx, ident := range payload { - logger.Debug("Lookup", "entity", ident.GetEntityType()) + logger.Debug("lookup", "entity", ident.GetEntityType()) var keycloakEntities []*gocloak.User var getUserParams gocloak.GetUsersParams exactMatch := true switch ident.GetEntityType().(type) { case *authorization.Entity_ClientId: - logger.Debug("GetClient", "client_id", ident.GetClientId()) + logger.Debug("looking up", slog.Any("type", ident.GetEntityType()), slog.String("client_id", ident.GetClientId())) clientID := ident.GetClientId() clients, err := connector.client.GetClients(ctx, connector.token.AccessToken, kcConfig.Realm, gocloak.GetClientsParams{ ClientID: &clientID, }) if err != nil { - logger.Error(err.Error()) + logger.Error("error getting client info", slog.String("error", err.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextGetRetrievalFailed) } @@ -108,13 +122,13 @@ func EntityResolution(ctx context.Context, for _, client := range clients { json, err := typeToGenericJSONMap(client, logger) if err != nil { - logger.Error("Error serializing entity representation!", "error", err) + logger.Error("error serializing entity representation!", slog.String("error", err.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed) } - var mystruct, structErr = structpb.NewStruct(json) + mystruct, structErr := structpb.NewStruct(json) if structErr != nil { - logger.Error("Error making struct!", "error", err) + logger.Error("error making struct!", slog.String("error", structErr.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed) } @@ -124,7 +138,7 @@ func EntityResolution(ctx context.Context, // convert entity to json entityStruct, err := entityToStructPb(ident) if err != nil { - logger.Error("unable to make entity struct", "error", err) + logger.Error("unable to make entity struct", slog.String("error", err.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed) } jsonEntities = append(jsonEntities, entityStruct) @@ -157,12 +171,12 @@ func EntityResolution(ctx context.Context, status.Error(codes.Internal, ErrTextGetRetrievalFailed) case len(users) == 1: user := users[0] - logger.Debug("User found", "user", *user.ID, "entity", ident.String()) - logger.Debug("User", "details", fmt.Sprintf("%+v", user)) - logger.Debug("User", "attributes", fmt.Sprintf("%+v", user.Attributes)) + logger.Debug("user found", slog.String("user", *user.ID), slog.String("entity", ident.String())) + logger.Debug("user", slog.Any("details", user)) + logger.Debug("user", slog.Any("attributes", user.Attributes)) keycloakEntities = append(keycloakEntities, user) default: - logger.Error("No user found for", "entity", ident) + logger.Error("no user found for", slog.Any("entity", ident)) if ident.GetEmailAddress() != "" { //nolint:nestif // this case has many possible outcomes to handle // try by group groups, groupErr := connector.client.GetGroups( @@ -173,11 +187,11 @@ func EntityResolution(ctx context.Context, ) switch { case groupErr != nil: - logger.Error("Error getting group", "group", groupErr) + logger.Error("error getting group", slog.String("group", groupErr.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextGetRetrievalFailed) case len(groups) == 1: - logger.Info("Group found for", "entity", ident.String()) + logger.Info("group found for", slog.String("entity", ident.String())) group := groups[0] expandedRepresentations, exErr := expandGroup(ctx, *group.ID, connector, &kcConfig, logger) if exErr != nil { @@ -187,7 +201,7 @@ func EntityResolution(ctx context.Context, keycloakEntities = expandedRepresentations } default: - logger.Error("No group found for", "entity", ident.String()) + logger.Error("no group found for", slog.String("entity", ident.String())) var entityNotFoundErr entityresolution.EntityNotFoundError switch ident.GetEntityType().(type) { case *authorization.Entity_EmailAddress: @@ -198,7 +212,7 @@ func EntityResolution(ctx context.Context, // return &entityresolution.IdpPluginResponse{}, // status.Error(codes.InvalidArgument, db.ErrTextNotFound) default: - logger.Error("Unsupported/unknown type for", "entity", ident.String()) + logger.Error("unsupported/unknown type for", slog.String("entity", ident.String())) entityNotFoundErr = entityresolution.EntityNotFoundError{Code: int32(codes.NotFound), Message: ErrTextGetRetrievalFailed, Entity: ident.String()} } logger.Error(entityNotFoundErr.String()) @@ -206,7 +220,7 @@ func EntityResolution(ctx context.Context, // user not found -- add json entity to resp instead entityStruct, err := entityToStructPb(ident) if err != nil { - logger.Error("unable to make entity struct from email or username", "error", err) + logger.Error("unable to make entity struct from email or username", slog.String("error", err.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed) } jsonEntities = append(jsonEntities, entityStruct) @@ -219,7 +233,7 @@ func EntityResolution(ctx context.Context, // user not found -- add json entity to resp instead entityStruct, err := entityToStructPb(ident) if err != nil { - logger.Error("unable to make entity struct from username", "error", err) + logger.Error("unable to make entity struct from username", slog.String("error", err.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed) } jsonEntities = append(jsonEntities, entityStruct) @@ -233,13 +247,13 @@ func EntityResolution(ctx context.Context, for _, er := range keycloakEntities { json, err := typeToGenericJSONMap(er, logger) if err != nil { - logger.Error("Error serializing entity representation!", "error", err) + logger.Error("error serializing entity representation!", slog.String("error", err.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed) } - var mystruct, structErr = structpb.NewStruct(json) + mystruct, structErr := structpb.NewStruct(json) if structErr != nil { - logger.Error("Error making struct!", "error", err) + logger.Error("error making struct!", slog.String("error", structErr.Error())) return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed) } @@ -254,9 +268,10 @@ func EntityResolution(ctx context.Context, resolvedEntities, &entityresolution.EntityRepresentation{ OriginalId: originialID, - AdditionalProps: jsonEntities}, + AdditionalProps: jsonEntities, + }, ) - logger.Debug("Entities", "resolved", fmt.Sprintf("%+v", resolvedEntities)) + logger.Debug("entities", slog.Any("resolved", resolvedEntities)) } return entityresolution.ResolveEntitiesResponse{ @@ -268,14 +283,14 @@ func typeToGenericJSONMap[Marshalable any](inputStruct Marshalable, logger *logg // For now, since we dont' know the "shape" of the entity/user record or representation we will get from a specific entity store, tmpDoc, err := json.Marshal(inputStruct) if err != nil { - logger.Error("Error marshalling input type!", "error", err) + logger.Error("error marshalling input type!", slog.String("error", err.Error())) return nil, err } var genericMap map[string]interface{} err = json.Unmarshal(tmpDoc, &genericMap) if err != nil { - logger.Error("Could not deserialize generic entitlement context JSON input document!", "error", err) + logger.Error("could not deserialize generic entitlement context JSON input document!", slog.String("error", err.Error())) return nil, err } @@ -285,7 +300,7 @@ func typeToGenericJSONMap[Marshalable any](inputStruct Marshalable, logger *logg func getKCClient(ctx context.Context, kcConfig KeycloakConfig, logger *logger.Logger) (*KeyCloakConnector, error) { var client *gocloak.GoCloak if kcConfig.LegacyKeycloak { - logger.Warn("Using legacy connection mode for Keycloak < 17.x.x") + logger.Warn("using legacy connection mode for Keycloak < 17.x.x") client = gocloak.NewClient(kcConfig.URL) } else { client = gocloak.NewClient(kcConfig.URL, gocloak.SetAuthAdminRealms("admin/realms"), gocloak.SetAuthRealms("realms")) @@ -303,7 +318,7 @@ func getKCClient(ctx context.Context, kcConfig KeycloakConfig, logger *logger.Lo // logger.Debug(kcConfig.Realm) token, err := client.LoginClient(ctx, kcConfig.ClientID, kcConfig.ClientSecret, kcConfig.Realm) if err != nil { - logger.Error("Error connecting to keycloak!", "error", err) + logger.Error("error connecting to keycloak!", slog.String("error", err.Error())) return nil, err } keycloakConnector := KeyCloakConnector{token: token, client: client} @@ -312,7 +327,7 @@ func getKCClient(ctx context.Context, kcConfig KeycloakConfig, logger *logger.Lo } func expandGroup(ctx context.Context, groupID string, kcConnector *KeyCloakConnector, kcConfig *KeycloakConfig, logger *logger.Logger) ([]*gocloak.User, error) { - logger.Info("expandGroup invoked: ", groupID, kcConnector, kcConfig.URL, ctx) + logger.Info("expanding group", slog.String("groupID", groupID)) var entityRepresentations []*gocloak.User grp, err := kcConnector.client.GetGroup(ctx, kcConnector.token.AccessToken, kcConfig.Realm, groupID) @@ -320,16 +335,16 @@ func expandGroup(ctx context.Context, groupID string, kcConnector *KeyCloakConne grpMembers, memberErr := kcConnector.client.GetGroupMembers(ctx, kcConnector.token.AccessToken, kcConfig.Realm, *grp.ID, gocloak.GetGroupsParams{}) if memberErr == nil { - logger.Debug("Adding members", "amount", len(grpMembers), "from group", *grp.Name) + logger.Debug("adding members", slog.Int("amount", len(grpMembers)), slog.String("from group", *grp.Name)) for i := 0; i < len(grpMembers); i++ { user := grpMembers[i] entityRepresentations = append(entityRepresentations, user) } } else { - logger.Error("Error getting group members", "error", memberErr) + logger.Error("error getting group members", slog.String("error", memberErr.Error())) } } else { - logger.Error("Error getting group", "error", err) + logger.Error("error getting group", slog.String("error", err.Error())) return nil, err } return entityRepresentations, nil @@ -344,8 +359,8 @@ func getEntitiesFromToken(ctx context.Context, kcConfig KeycloakConfig, jwtStrin if err != nil { return nil, errors.New("error getting claims from jwt") } - var entities = []*authorization.Entity{} - var entityID = 0 + entities := []*authorization.Entity{} + entityID := 0 // extract azp extractedValue, okExtract := claims[ClientJwtSelector] @@ -359,7 +374,8 @@ func getEntitiesFromToken(ctx context.Context, kcConfig KeycloakConfig, jwtStrin entities = append(entities, &authorization.Entity{ EntityType: &authorization.Entity_ClientId{ClientId: extractedValueCasted}, Id: fmt.Sprintf("jwtentity-%d", entityID), - Category: authorization.Entity_CATEGORY_ENVIRONMENT}) + Category: authorization.Entity_CATEGORY_ENVIRONMENT, + }) entityID++ extractedValueUsername, okExp := claims[UsernameJwtSelector] @@ -381,19 +397,22 @@ func getEntitiesFromToken(ctx context.Context, kcConfig KeycloakConfig, jwtStrin entities = append(entities, &authorization.Entity{ EntityType: &authorization.Entity_ClientId{ClientId: clientid}, Id: fmt.Sprintf("jwtentity-%d", entityID), - Category: authorization.Entity_CATEGORY_SUBJECT}) + Category: authorization.Entity_CATEGORY_SUBJECT, + }) } else { // if the returned clientId is empty, no client found, its not a serive account proceed with username entities = append(entities, &authorization.Entity{ EntityType: &authorization.Entity_UserName{UserName: extractedValueUsernameCasted}, Id: fmt.Sprintf("jwtentity-%d", entityID), - Category: authorization.Entity_CATEGORY_SUBJECT}) + Category: authorization.Entity_CATEGORY_SUBJECT, + }) } } else { entities = append(entities, &authorization.Entity{ EntityType: &authorization.Entity_UserName{UserName: extractedValueUsernameCasted}, Id: fmt.Sprintf("jwtentity-%d", entityID), - Category: authorization.Entity_CATEGORY_SUBJECT}) + Category: authorization.Entity_CATEGORY_SUBJECT, + }) } return entities, nil @@ -415,12 +434,12 @@ func getServiceAccountClient(ctx context.Context, username string, kcConfig Keyc return "", err case len(clients) == 1: client := clients[0] - logger.Debug("Client found", "client", *client.ClientID) + logger.Debug("client found", slog.String("client", *client.ClientID)) return *client.ClientID, nil case len(clients) > 1: - logger.Error("More than one client found for ", "clientid", expectedClientName) + logger.Error("more than one client found for ", slog.String("clientid", expectedClientName)) default: - logger.Debug("No client found, likely not a service account", "clientid", expectedClientName) + logger.Debug("no client found, likely not a service account", slog.String("clientid", expectedClientName)) } return "", nil diff --git a/service/internal/auth/config.go b/service/internal/auth/config.go index f50d0e3c5a..63d7abbb02 100644 --- a/service/internal/auth/config.go +++ b/service/internal/auth/config.go @@ -9,29 +9,29 @@ import ( // AuthConfig pulls AuthN and AuthZ together type Config struct { - Enabled bool `yaml:"enabled" default:"true" ` + Enabled bool `mapstructure:"enabled" json:"enabled" default:"true" ` PublicRoutes []string `mapstructure:"-"` AuthNConfig `mapstructure:",squash"` } // AuthNConfig is the configuration need for the platform to validate tokens type AuthNConfig struct { //nolint:revive // AuthNConfig is a valid name - EnforceDPoP bool `yaml:"enforceDPoP" json:"enforceDPoP" mapstructure:"enforceDPoP" default:"false"` - Issuer string `yaml:"issuer" json:"issuer"` - Audience string `yaml:"audience" json:"audience"` - Policy PolicyConfig `yaml:"policy" json:"policy" mapstructure:"policy"` + EnforceDPoP bool `mapstructure:"enforceDPoP" json:"enforceDPoP" default:"false"` + Issuer string `mapstructure:"issuer" json:"issuer"` + Audience string `mapstructure:"audience" json:"audience"` + Policy PolicyConfig `mapstructure:"policy" json:"policy"` CacheRefresh string `mapstructure:"cache_refresh_interval"` DPoPSkew time.Duration `mapstructure:"dpopskew" default:"1h"` TokenSkew time.Duration `mapstructure:"skew" default:"1m"` - PublicClientID string `yaml:"public_client_id" json:"public_client_id,omitempty" mapstructure:"public_client_id"` + PublicClientID string `mapstructure:"public_client_id" json:"public_client_id,omitempty"` } type PolicyConfig struct { - Default string `yaml:"default" json:"default"` - RoleClaim string `yaml:"claim" json:"claim" mapstructure:"claim"` - RoleMap map[string]string `yaml:"map" json:"map" mapstructure:"map"` - Csv string `yaml:"csv" json:"csv"` - Model string `yaml:"model" json:"model"` + Default string `mapstructure:"default" json:"default"` + RoleClaim string `mapstructure:"claim" json:"claim"` + RoleMap map[string]string `mapstructure:"map" json:"map"` + Csv string `mapstructure:"csv" json:"csv"` + Model string `mapstructure:"model" json:"model"` } func (c AuthNConfig) validateAuthNConfig(logger *logger.Logger) error { diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 0eb776a997..49b93df94f 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -5,7 +5,6 @@ import ( "fmt" "log/slog" "os" - "reflect" "strings" "github.com/creasty/defaults" @@ -14,30 +13,29 @@ import ( "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/db" "github.com/opentdf/platform/service/pkg/serviceregistry" - "github.com/opentdf/platform/service/pkg/util" "github.com/spf13/viper" ) // Config represents the configuration settings for the service. type Config struct { // DevMode specifies whether the service is running in development mode. - DevMode bool `mapstructure:"dev_mode"` + DevMode bool `mapstructure:"dev_mode" json:"dev_mode"` // DB represents the configuration settings for the database. - DB db.Config `mapstructure:"db"` + DB db.Config `mapstructure:"db" json:"db"` // Server represents the configuration settings for the server. - Server server.Config `mapstructure:"server"` + Server server.Config `mapstructure:"server" json:"server"` // Logger represents the configuration settings for the logger. - Logger logger.Config `mapstructure:"logger"` + Logger logger.Config `mapstructure:"logger" json:"logger"` // Mode specifies which services to run. // By default, it runs all services. - Mode []string `mapstructure:"mode" default:"[\"all\"]"` + Mode []string `mapstructure:"mode" json:"mode" default:"[\"all\"]"` // SDKConfig represents the configuration settings for the SDK. - SDKConfig SDKConfig `mapstructure:"sdk_config"` + SDKConfig SDKConfig `mapstructure:"sdk_config" json:"sdk_config"` // Services represents the configuration settings for the services. Services map[string]serviceregistry.ServiceConfig `mapstructure:"services"` @@ -46,18 +44,18 @@ type Config struct { // SDKConfig represents the configuration for the SDK. type SDKConfig struct { // Endpoint is the URL of the Core Platform endpoint. - Endpoint string `mapstructure:"endpoint"` + Endpoint string `mapstructure:"endpoint" json:"endpoint"` // Plaintext specifies whether the SDK should use plaintext communication. - Plaintext bool `mapstructure:"plaintext" default:"false" validate:"boolean"` + Plaintext bool `mapstructure:"plaintext" json:"plaintext" default:"false" validate:"boolean"` // ClientID is the client ID used for client credentials grant. // It is required together with ClientSecret. - ClientID string `mapstructure:"client_id" validate:"required_with=ClientSecret"` + ClientID string `mapstructure:"client_id" json:"client_id" validate:"required_with=ClientSecret"` // ClientSecret is the client secret used for client credentials grant. // It is required together with ClientID. - ClientSecret string `mapstructure:"client_secret" validate:"required_with=ClientID"` + ClientSecret string `mapstructure:"client_secret" json:"client_secret" validate:"required_with=ClientID"` } type Error string @@ -124,20 +122,24 @@ func LoadConfig(key, file string) (*Config, error) { return config, nil } +// LogValue returns a slog.Value representation of the config. +// We exclude logging service configuration as it may contain sensitive information. func (c *Config) LogValue() slog.Value { - redactedConfig := util.RedactSensitiveData(c) - var values []slog.Attr - v := reflect.ValueOf(redactedConfig).Elem() - t := v.Type() - - for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - fieldType := t.Field(i) - key := fieldType.Tag.Get("yaml") - if key == "" { - key = fieldType.Name - } - values = append(values, slog.String(key, util.StructToString(field))) - } - return slog.GroupValue(values...) + return slog.GroupValue( + slog.Bool("dev_mode", c.DevMode), + slog.Any("db", c.DB), + slog.Any("logger", c.Logger), + slog.Any("mode", c.Mode), + slog.Any("sdk_config", c.SDKConfig), + slog.Any("server", c.Server), + ) +} + +func (c SDKConfig) LogValue() slog.Value { + return slog.GroupValue( + slog.String("endpoint", c.Endpoint), + slog.Bool("plaintext", c.Plaintext), + slog.String("client_id", c.ClientID), + slog.String("client_secret", "[REDACTED]"), + ) } diff --git a/service/internal/security/standard_crypto.go b/service/internal/security/standard_crypto.go index 7570cebc09..2570a98b10 100644 --- a/service/internal/security/standard_crypto.go +++ b/service/internal/security/standard_crypto.go @@ -21,33 +21,33 @@ const ( ) type StandardConfig struct { - Keys []KeyPairInfo `mapstructure:"keys"` + Keys []KeyPairInfo `mapstructure:"keys" json:"keys"` // Deprecated - RSAKeys map[string]StandardKeyInfo `yaml:"rsa,omitempty" mapstructure:"rsa"` + RSAKeys map[string]StandardKeyInfo `mapstructure:"rsa,omitempty" json:"rsa,omitempty"` // Deprecated - ECKeys map[string]StandardKeyInfo `yaml:"ec,omitempty" mapstructure:"ec"` + ECKeys map[string]StandardKeyInfo `mapstructure:"ec,omitempty" json:"ec,omitempty"` } type KeyPairInfo struct { // Valid algorithm. May be able to be derived from Private but it is better to just say it. - Algorithm string `mapstructure:"alg"` + Algorithm string `mapstructure:"alg" json:"alg"` // Key identifier. Should be short - KID string `mapstructure:"kid"` + KID string `mapstructure:"kid" json:"kid"` // Implementation specific locator for private key; // for 'standard' crypto service this is the path to a PEM file - Private string `mapstructure:"private"` + Private string `mapstructure:"private" json:"private"` // Optional locator for the corresponding certificate. // If not found, only public key (derivable from Private) is available. - Certificate string `mapstructure:"cert"` + Certificate string `mapstructure:"cert" json:"cert"` // Optional enumeration of intended usages of keypair - Usage string `mapstructure:"usage"` + Usage string `mapstructure:"usage" json:"usage"` // Optional long form description of key pair including purpose and life cycle information - Purpose string `mapstructure:"purpose"` + Purpose string `mapstructure:"purpose" json:"purpose"` } type StandardKeyInfo struct { - PrivateKeyPath string `yaml:"private_key_path" mapstructure:"private_key_path"` - PublicKeyPath string `yaml:"public_key_path" mapstructure:"public_key_path"` + PrivateKeyPath string `mapstructure:"private_key_path" json:"private_key_path"` + PublicKeyPath string `mapstructure:"public_key_path" json:"public_key_path"` } type StandardRSACrypto struct { diff --git a/service/internal/security/standard_only.go b/service/internal/security/standard_only.go index b95269c50e..a4b49b3b29 100644 --- a/service/internal/security/standard_only.go +++ b/service/internal/security/standard_only.go @@ -3,9 +3,9 @@ package security import "log/slog" type Config struct { - Type string `yaml:"type" default:"standard"` + Type string `mapstructure:"type" json:"type" default:"standard"` // StandardConfig is the configuration for the standard key provider - StandardConfig StandardConfig `yaml:"standard,omitempty" mapstructure:"standard"` + StandardConfig StandardConfig `mapstructure:"standard" json:"standard"` } func NewCryptoProvider(cfg Config) (CryptoProvider, error) { diff --git a/service/internal/server/server.go b/service/internal/server/server.go index c4d87f55a5..e046d4c0bd 100644 --- a/service/internal/server/server.go +++ b/service/internal/server/server.go @@ -8,12 +8,11 @@ import ( "log/slog" "net" "net/http" + "net/http/pprof" "net/netip" "strings" "time" - "net/http/pprof" - "github.com/bufbuild/protovalidate-go" "github.com/go-chi/cors" protovalidate_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate" @@ -48,48 +47,61 @@ func (e Error) Error() string { // Configurations for the server type Config struct { - Auth auth.Config `yaml:"auth"` - GRPC GRPCConfig `yaml:"grpc"` - CryptoProvider security.Config `yaml:"cryptoProvider"` - TLS TLSConfig `yaml:"tls"` - CORS CORSConfig `yaml:"cors"` - WellKnownConfigRegister func(namespace string, config any) error + Auth auth.Config `mapstructure:"auth" json:"auth"` + GRPC GRPCConfig `mapstructure:"grpc" json:"grpc"` + CryptoProvider security.Config `mapstructure:"cryptoProvider" json:"cryptoProvider"` + TLS TLSConfig `mapstructure:"tls" json:"tls"` + CORS CORSConfig `mapstructure:"cors" json:"cors"` + WellKnownConfigRegister func(namespace string, config any) error `mapstructure:"-" json:"-"` // Port to listen on - Port int `yaml:"port" default:"8080"` - Host string `yaml:"host,omitempty"` + Port int `mapstructure:"port" json:"port" default:"8080"` + Host string `mapstructure:"host,omitempty" json:"host"` // Enable pprof - EnablePprof bool `mapstructure:"enable_pprof" default:"false"` + EnablePprof bool `mapstructure:"enable_pprof" json:"enable_pprof" default:"false"` +} + +func (c Config) LogValue() slog.Value { + return slog.GroupValue( + slog.Any("auth", c.Auth), + slog.Any("grpc", c.GRPC), + slog.Any("cryptoProvider", c.CryptoProvider), + slog.Any("tls", c.TLS), + slog.Any("cors", c.CORS), + slog.Int("port", c.Port), + slog.String("host", c.Host), + slog.Bool("enablePprof", c.EnablePprof), + ) } // GRPC Server specific configurations type GRPCConfig struct { // Enable reflection for grpc server (default: true) - ReflectionEnabled bool `yaml:"reflectionEnabled" default:"true"` + ReflectionEnabled bool `mapstructure:"reflectionEnabled" json:"reflectionEnabled" default:"true"` - MaxCallRecvMsgSizeBytes int `yaml:"maxCallRecvMsgSize" default:"4194304"` // 4MB = 4 * 1024 * 1024 = 4194304 - MaxCallSendMsgSizeBytes int `yaml:"maxCallSendMsgSize" default:"4194304"` // 4MB = 4 * 1024 * 1024 = 4194304 + MaxCallRecvMsgSizeBytes int `mapstructure:"maxCallRecvMsgSize" json:"maxCallRecvMsgSize" default:"4194304"` // 4MB = 4 * 1024 * 1024 = 4194304 + MaxCallSendMsgSizeBytes int `mapstructure:"maxCallSendMsgSize" json:"maxCallSendMsgSize" default:"4194304"` // 4MB = 4 * 1024 * 1024 = 4194304 } // TLS Configuration for the server type TLSConfig struct { // Enable TLS for the server (default: false) - Enabled bool `yaml:"enabled" default:"false"` + Enabled bool `mapstructure:"enabled" json:"enabled" default:"false"` // Path to the certificate file - Cert string `yaml:"cert"` + Cert string `mapstructure:"cert" json:"cert"` // Path to the key file - Key string `yaml:"key"` + Key string `mapstructure:"key" json:"key"` } // CORS Configuration for the server type CORSConfig struct { // Enable CORS for the server (default: true) - Enabled bool `yaml:"enabled" default:"true"` - AllowedOrigins []string `yaml:"allowedorigins"` - AllowedMethods []string `yaml:"allowedmethods"` - AllowedHeaders []string `yaml:"allowedheaders"` - ExposedHeaders []string `yaml:"exposedheaders"` - AllowCredentials bool `yaml:"allowcredentials" default:"true"` - MaxAge int `yaml:"maxage" default:"3600"` + Enabled bool `mapstructure:"enabled" json:"enabled" default:"true"` + AllowedOrigins []string `mapstructure:"allowedorigins" json:"allowedorigins"` + AllowedMethods []string `mapstructure:"allowedmethods" json:"allowedmethods"` + AllowedHeaders []string `mapstructure:"allowedheaders" json:"allowedheaders"` + ExposedHeaders []string `mapstructure:"exposedheaders" json:"exposedheaders"` + AllowCredentials bool `mapstructure:"allowcredentials" json:"allowedcredentials" default:"true"` + MaxAge int `mapstructure:"maxage" json:"maxage" default:"3600"` } type OpenTDFServer struct { @@ -271,7 +283,7 @@ func pprofHandler(h http.Handler) http.Handler { // httpGrpcHandlerFunc returns a http.Handler that delegates to the grpc server if the request is a grpc request func httpGrpcHandlerFunc(h http.Handler, g *grpc.Server, l *logger.Logger) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - l.Debug("grpc handler func", slog.Int("proto_major", r.ProtoMajor), slog.String("content_type", r.Header.Get("Content-Type"))) + l.TraceContext(r.Context(), "grpc handler func", slog.Int("proto_major", r.ProtoMajor), slog.String("content_type", r.Header.Get("Content-Type"))) if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { g.ServeHTTP(w, r) } else { diff --git a/service/kas/access/provider.go b/service/kas/access/provider.go index ab7c0dad10..7bfc7acb5a 100644 --- a/service/kas/access/provider.go +++ b/service/kas/access/provider.go @@ -29,20 +29,20 @@ type Provider struct { type KASConfig struct { // Which keys are currently the default. - Keyring []CurrentKeyFor `mapstructure:"keyring"` + Keyring []CurrentKeyFor `mapstructure:"keyring" json:"keyring"` // Deprecated - ECCertID string `mapstructure:"eccertid"` + ECCertID string `mapstructure:"eccertid" json:"eccertid"` // Deprecated - RSACertID string `mapstructure:"rsacertid"` + RSACertID string `mapstructure:"rsacertid" json:"rsacertid"` } // Specifies the preferred/default key for a given algorithm type. type CurrentKeyFor struct { - Algorithm string `mapstructure:"alg"` - KID string `mapstructure:"kid"` + Algorithm string `mapstructure:"alg" json:"alg"` + KID string `mapstructure:"kid" json:"kid"` // Indicates that the key should not be serves by default, // but instead is allowed for legacy reasons on decrypt (rewrap) only - Legacy bool `mapstructure:"legacy"` + Legacy bool `mapstructure:"legacy" json:"legacy"` } func (p *Provider) IsReady(ctx context.Context) error { diff --git a/service/kas/kas.go b/service/kas/kas.go index 14d21d0ea8..8cb3c14963 100644 --- a/service/kas/kas.go +++ b/service/kas/kas.go @@ -73,6 +73,8 @@ func NewRegistration() serviceregistry.Registration { KASConfig: kasCfg, } + srp.Logger.Debug("kas config", "config", kasCfg) + if err := srp.RegisterReadinessCheck("kas", p.IsReady); err != nil { srp.Logger.Error("failed to register kas readiness check", slog.String("error", err.Error())) } diff --git a/service/logger/logger.go b/service/logger/logger.go index c17f341a40..30c7661001 100644 --- a/service/logger/logger.go +++ b/service/logger/logger.go @@ -35,9 +35,9 @@ type Logger struct { } type Config struct { - Level string `yaml:"level" default:"info"` - Output string `yaml:"output" default:"stdout"` - Type string `yaml:"type" default:"json"` + Level string `mapstructure:"level" json:"level" default:"info"` + Output string `mapstructure:"output" json:"output" default:"stdout"` + Type string `mapstructure:"type" json:"type" default:"json"` } const ( @@ -45,7 +45,8 @@ const ( ) func NewLogger(config Config) (*Logger, error) { - var logger *slog.Logger + var sLogger *slog.Logger + logger := new(Logger) w, err := getWriter(config) if err != nil { @@ -61,15 +62,15 @@ func NewLogger(config Config) (*Logger, error) { case "json": j := slog.NewJSONHandler(w, &slog.HandlerOptions{ Level: level, - ReplaceAttr: audit.ReplaceAttrAuditLevel, + ReplaceAttr: logger.replaceAttrChain, }) - logger = slog.New(j) + sLogger = slog.New(j) case "text": t := slog.NewTextHandler(w, &slog.HandlerOptions{ Level: level, - ReplaceAttr: audit.ReplaceAttrAuditLevel, + ReplaceAttr: logger.replaceAttrChain, }) - logger = slog.New(t) + sLogger = slog.New(t) default: return nil, fmt.Errorf("invalid logger type: %s", config.Type) } @@ -83,10 +84,10 @@ func NewLogger(config Config) (*Logger, error) { auditLoggerBase := slog.New(auditLoggerHandler) auditLogger := audit.CreateAuditLogger(*auditLoggerBase) - return &Logger{ - Logger: logger, - Audit: auditLogger, - }, nil + logger.Logger = sLogger + logger.Audit = auditLogger + + return logger, nil } func (l *Logger) With(key string, value string) *Logger { @@ -136,3 +137,8 @@ func CreateTestLogger() *Logger { }) return logger } + +// TODO: We can filter by keys if we need to in the future so they don't get proccessed by the masqer +func (l *Logger) replaceAttrChain(groups []string, a slog.Attr) slog.Attr { + return audit.ReplaceAttrAuditLevel(groups, a) +} diff --git a/service/pkg/db/db.go b/service/pkg/db/db.go index 8f8fec0708..79cb08a708 100644 --- a/service/pkg/db/db.go +++ b/service/pkg/db/db.go @@ -63,17 +63,31 @@ type PgxIface interface { } type Config struct { - Host string `yaml:"host" default:"localhost"` - Port int `yaml:"port" default:"5432"` - Database string `yaml:"database" default:"opentdf"` - User string `yaml:"user" default:"postgres"` - Password string `yaml:"password" default:"changeme" secret:"true"` - RunMigrations bool `yaml:"runMigrations" default:"true"` - SSLMode string `yaml:"sslmode" default:"prefer"` - Schema string `yaml:"schema" default:"opentdf"` - - VerifyConnection bool `yaml:"verifyConnection" default:"true"` - MigrationsFS *embed.FS + Host string `mapstructure:"host" json:"host" default:"localhost"` + Port int `mapstructure:"port" json:"port" default:"5432"` + Database string `mapstructure:"database" json:"database" default:"opentdf"` + User string `mapstructure:"user" json:"user" default:"postgres"` + Password string `mapstructure:"password" json:"password" default:"changeme"` + RunMigrations bool `mapstructure:"runMigrations" json:"runMigrations" default:"true"` + SSLMode string `mapstructure:"sslmode" json:"sslmode" default:"prefer"` + Schema string `mapstructure:"schema" json:"schema" default:"opentdf"` + + VerifyConnection bool `mapstructure:"verifyConnection" json:"verifyConnection" default:"true"` + MigrationsFS *embed.FS `mapstructure:"-"` +} + +func (c Config) LogValue() slog.Value { + return slog.GroupValue( + slog.String("host", c.Host), + slog.Int("port", c.Port), + slog.String("database", c.Database), + slog.String("user", c.User), + slog.String("password", "[REDACTED]"), + slog.String("sslmode", c.SSLMode), + slog.String("schema", c.Schema), + slog.Bool("runMigrations", c.RunMigrations), + slog.Bool("verifyConnection", c.VerifyConnection), + ) } /* diff --git a/service/pkg/util/redact.go b/service/pkg/util/redact.go deleted file mode 100644 index 5148f5397a..0000000000 --- a/service/pkg/util/redact.go +++ /dev/null @@ -1,106 +0,0 @@ -package util - -import ( - "fmt" - "reflect" - "strings" -) - -func RedactSensitiveData(i interface{}) interface{} { - v := reflect.ValueOf(i) - redacted := redact(v) - return redacted.Interface() -} - -func redact(v reflect.Value) reflect.Value { - //nolint:exhaustive // default case covers other type - switch v.Kind() { - case reflect.Ptr: - if v.IsNil() { - return v - } - redacted := reflect.New(v.Elem().Type()) - redacted.Elem().Set(redact(v.Elem())) - return redacted - case reflect.Struct: - redacted := reflect.New(v.Type()).Elem() - for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - fieldType := v.Type().Field(i) - tag := fieldType.Tag.Get("secret") - if tag == "true" { - // Redact sensitive fields - redacted.Field(i).SetString("***") - } else { - // Recursively redact nested fields - redacted.Field(i).Set(redact(field)) - } - } - return redacted - case reflect.Map: - redacted := reflect.MakeMap(v.Type()) - for _, key := range v.MapKeys() { - val := v.MapIndex(key) - redacted.SetMapIndex(key, redact(val)) - } - return redacted - case reflect.Slice: - redacted := reflect.MakeSlice(v.Type(), v.Len(), v.Cap()) - for i := 0; i < v.Len(); i++ { - redacted.Index(i).Set(redact(v.Index(i))) - } - return redacted - default: - return v - } -} - -func StructToString(v reflect.Value) string { - var b strings.Builder - //nolint:exhaustive // default case covers other type - switch v.Kind() { - case reflect.Ptr: - if v.IsNil() { - return "" - } - return StructToString(v.Elem()) - case reflect.Struct: - b.WriteString("{") - t := v.Type() - for i := 0; i < v.NumField(); i++ { - if i > 0 { - b.WriteString(" ") - } - field := v.Field(i) - fieldType := t.Field(i) - b.WriteString(fieldType.Name) - b.WriteString(":") - b.WriteString(StructToString(field)) - } - b.WriteString("}") - return b.String() - case reflect.Map: - b.WriteString("map[") - keys := v.MapKeys() - for i, key := range keys { - if i > 0 { - b.WriteString(" ") - } - b.WriteString(fmt.Sprintf("%v:%v", key, StructToString(v.MapIndex(key)))) - } - b.WriteString("]") - return b.String() - case reflect.Slice: - b.WriteString("[") - for i := 0; i < v.Len(); i++ { - if i > 0 { - b.WriteString(" ") - } - b.WriteString(StructToString(v.Index(i))) - } - b.WriteString("]") - return b.String() - default: - return fmt.Sprintf("%v", v.Interface()) - } -} diff --git a/service/pkg/util/redact_test.go b/service/pkg/util/redact_test.go deleted file mode 100644 index acaab76bc8..0000000000 --- a/service/pkg/util/redact_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package util - -import ( - "encoding/json" - "testing" -) - -type DBConfig struct { - Host string `json:"host"` - Port int `json:"port"` - Database string `json:"database"` - User string `json:"user"` - Password string `json:"password" secret:"true"` - RunMigrations bool `json:"runMigrations"` - SSLMode string `json:"sslmode"` - Schema string `json:"schema"` - VerifyConnection bool `json:"verifyConnection"` -} - -type Config struct { - DevMode bool `json:"devMode"` - DB DBConfig `json:"db"` - Services map[string]struct { - Enabled bool `json:"enabled"` - Remote struct { - Endpoint string `json:"endpoint"` - } `json:"remote"` - ExtraProps map[string]interface{} `json:"extraProps"` - } `json:"services"` -} - -func TestRedactSensitiveData_WithSensitiveFieldsInNestedStruct(t *testing.T) { - rawConfig := `{ - "DevMode": false, - "DB": { - "Host": "localhost", - "Port": 5432, - "Database": "opentdf", - "User": "postgres", - "Password": "changeme", - "RunMigrations": true, - "SSLMode": "prefer", - "Schema": "opentdf", - "VerifyConnection": true - }, - "Services": { - "authorization": { - "Enabled": true, - "Remote": { - "Endpoint": "" - }, - "ExtraProps": { - "clientid": "tdf-authorization-svc", - "clientsecret": "secret", - "ersurl": "http://localhost:8080/entityresolution/resolve", - "tokenendpoint": "http://localhost:8888/auth/realms/opentdf/protocol/openid-connect/token" - } - }, - "entityresolution": { - "Enabled": true, - "Remote": { - "Endpoint": "" - }, - "ExtraProps": { - "clientid": "tdf-entity-resolution", - "clientsecret": "secret", - "legacykeycloak": true, - "realm": "opentdf", - "url": "http://localhost:8888/auth" - } - }, - "health": { - "Enabled": true, - "Remote": { - "Endpoint": "" - }, - "ExtraProps": {} - }, - "kas": { - "Enabled": true, - "Remote": { - "Endpoint": "" - }, - "ExtraProps": { - "keyring": [ - {"alg": "ec:secp256r1", "kid": "e1"}, - {"alg": "ec:secp256r1", "kid": "e1", "legacy": true}, - {"alg": "rsa:2048", "kid": "r1"}, - {"alg": "rsa:2048", "kid": "r1", "legacy": true} - ] - } - }, - "policy": { - "Enabled": true, - "Remote": { - "Endpoint": "" - }, - "ExtraProps": {} - }, - "wellknown": { - "Enabled": true, - "Remote": { - "Endpoint": "" - }, - "ExtraProps": {} - } - } - }` - - var config Config - err := json.Unmarshal([]byte(rawConfig), &config) - if err != nil { - t.Fatalf("Failed to unmarshal rawConfig: %v", err) - } - - redacted := RedactSensitiveData(config) - - redactedConfig, ok1 := redacted.(Config) - if !ok1 { - t.Fatalf("Expected redacted data to be of type Config") - } - - if redactedConfig.DB.Password != "***" { - t.Errorf("Expected DB.Password to be redacted") - } -}