diff --git a/rfc/bh-rfc-6.md b/rfc/bh-rfc-6.md new file mode 100644 index 00000000000..7ce6ea33ac8 --- /dev/null +++ b/rfc/bh-rfc-6.md @@ -0,0 +1,135 @@ +--- +bh-rfc: 6 +title: Dogtags - SKU-Based Feature Entitlements +authors: | + [Pomeroy, Kaleb](kpomeroy@specterops.io) +status: DRAFT +created: 2025-01-07 +--- + +# Dogtags - SKU-Based Feature Entitlements + +## 1. Overview + +Dogtags is a provider-based system for managing feature entitlements. It abstracts where SKU level configuration comes from, allowing the same application code to work whether entitlements are sourced from a license file, a database, or some future backend we haven't thought of yet. + +## 2. Motivation & Goals + +BloodHound supports multiple deployment models with different configuration needs. Some deployments read entitlements from signed license files, others from a database. The application shouldn't care where the values come from. + +- **Abstraction** - Decouple feature checks from configuration source +- **Extensibility** - Add new providers without touching application code +- **Simplicity** - One interface for all entitlement lookups + +## 3. Considerations + +### 3.1 Provider Injection + +Providers are injected at runtime. The application receives a configured provider at startup and uses it for all entitlement lookups. Provider implementation details are intentionally hidden from the application. The FOSS application uses a no-op provider, meaning default values in the SKU lookup field will apply. + +### 3.2 Startup-Time Loading + +Entitlements are loaded once at startup and cached. Changes require a restart. This is intentional; Runtime updates add complexity and create less predictable behavior. + +### 3.3 Fail-Fast + +If a provider can't load its configuration, the application fails to start. Silent fallbacks mask misconfigurations and make debugging painful. + +### 3.4 Public Interfaces, Private Implementations + +The dogtags interfaces and service layer live in this public repository, but the feature itself is enterprise-only. This is a deliberate pattern: CE ships with a `NoopProvider` that returns defaults, effectively disabling SKU-gated functionality. Enterprise builds inject real providers that source actual entitlements. + +This lets us maintain a single codebase where enterprise features are structurally present but functionally inert in CE. Application code doesn't branch on "is this enterprise?" - it just asks dogtags for values and gets sensible defaults in CE. + +## 4. Architecture + +### 4.1 Provider Interface + +Providers implement a simple interface: + +```go +type Provider interface { + GetFlagAsBool(key string) (bool, error) + GetFlagAsString(key string) (string, error) + GetFlagAsInt(key string) (int64, error) +} +``` + +Providers return errors when a key isn't found. This lets the service layer decide whether to use defaults. + +### 4.2 Service Layer + +The service wraps providers and handles defaults. If a provider doesn't have a flag, the service falls back to the default defined in the SKU spec. + +```go +type Service interface { + GetFlagAsBool(key BoolDogTag) bool + GetFlagAsString(key StringDogTag) string + GetFlagAsInt(key IntDogTag) int64 + GetAllDogTags() map[string]any +} +``` + +Note the typed keys (`BoolDogTag`, `IntDogTag`, etc.) - this prevents mixing up flag types at compile time. + +### 4.3 API Endpoint + +Dogtags are exposed via `GET /api/v2/dog-tags`. The response is always a flat map (no nested objects). Keys use dot notation for namespacing (`namespace.key`), but this is purely a naming convention, not structure. + +```json +{ + "data": { + "privilege_zones.tier_limit": 3, + "privilege_zones.label_limit": 10, + "privilege_zones.multi_tier_analysis": false + } +} +``` + +## 5. Adding a New SKU Flag + +### 5.1 Define the Flag + +Add to `bhce/cmd/api/src/services/dogtags/sku_flags.go`: + +```go +const ( + MY_NEW_FEATURE BoolDogTag = "my_feature.enabled" +) + +var AllBoolDogTags = map[BoolDogTag]BoolDogTagSpec{ + MY_NEW_FEATURE: {Description: "My New Feature", Default: false}, +} +``` + +Pick a sensible default. This is what the service returns if a provider doesn't have the flag. + +### 5.2 Configure Providers + +Every provider must be updated to source the new flag's value. Consult provider-specific documentation for configuration details. Missing this step means some deployments will silently fall back to defaults. + +### 5.3 Use It + +```go +if s.DogTags.GetFlagAsBool(dogtags.MY_NEW_FEATURE) { + // feature enabled +} +``` + +The service handles defaults, so you never need to check errors. + +## 6. Testing + +Use `NoopProvider` for tests - it always returns errors, forcing the service to use defaults. For testing specific flag values, create a mock provider. + +```go +dogtagsService := dogtags.NewDefaultService() // uses NoopProvider +``` + +## 7. File Locations + +| File | Purpose | +| ------------------------------------------------ | ------------------------------------ | +| `bhce/cmd/api/src/services/dogtags/sku_flags.go` | Flag definitions and defaults | +| `bhce/cmd/api/src/services/dogtags/service.go` | Service interface and implementation | +| `bhce/cmd/api/src/services/dogtags/provider.go` | Provider interface and NoopProvider |