Merge pull request #11 from vernonkeenan/agent/adr-kv-024-rollout

Add native service principal contract
pull/12/head v0.7.9
Vernon Keenan 2026-07-22 11:13:40 -07:00 committed by GitHub
commit 58ebde8660
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 98 additions and 9 deletions

View File

@ -44,13 +44,13 @@ pins the same module, a `lib` bump is a constellation-wide rebuild (KV-C6a).
| Path | What it is |
|---|---|
| `app/root.go` | `InitConfig(systemName, level)` — loads `/etc/vernonkeenan/<systemName>.yaml` via viper, one-time init guard |
| `app/config.go` | `Configuration` struct + `Get*`/`Is*` accessors (DBMS, DSN, log level, metrics, service accounts, workers, email, chunk/cache sizing) |
| `app/config.go` | `Configuration` struct + `Get*`/`Is*` accessors (DBMS, DSN, authentication mode, log level, metrics, service accounts, workers, email, chunk/cache sizing) |
| `app/mysql.go` | `InitDB()` — opens `MyDB *sql.DB` against the configured MySQL DSN |
| `app/force.go` | `InitForce(serviceAccountName)` — opens a Salesforce `go-force` API connection from a configured service account |
| `app/auth.go` | `CheckAPIUser(token)` — validates an API key against `auth`'s generated client |
| `app/auth.go` | `CheckAPIUser(token)` — validates a credential against `auth`; errors never log the raw value |
| `app/prometheus.go` | `SetupPrometheusHandler` — CORS + request-count/duration middleware |
| `app/service-helpers.go` | Pointer helpers (`String`/`Bool`/`Int64`/…), `GetFieldsAndValues` reflection helper (unit-tested) |
| `app/user.go`, `user-helpers.go`, `tenantuser.go`, `userrole.go`, `address.go` | First-class domain structs + swagger marshal/unmarshal helpers shared by consumers |
| `app/user.go`, `user-helpers.go`, `tenantuser.go`, `userrole.go`, `address.go` | Compatibility principal/domain structs. ADR-KV-024 adds `PrincipalKey`, `PrincipalType`, exact `Scopes`, `HasScope`, and `IsServicePrincipal`; no native response carries its bearer secret. |
| `app/logger/logger.go` | Thin `zap.SugaredLogger` wrapper (`New(level)`, level constants) |
| `api/{auth,crm,members,plex,research,sfgate,stash}/` | go-swaggergenerated typed clients + models, one directory per sibling service, regenerated from that service's own spec |
| `swagger/*.yaml`, `swagger/defs/`, `swagger/external/` | Copies of each sibling service's spec (the KV-C6b spec-copy problem — no sync mechanism besides `make regen`), shared component defs, and host/path-rewritten copies for external doc publishing; also carries legacy vendored Kazoo/Clerk specs from `lib`'s pre-mesh history |

View File

@ -148,6 +148,12 @@ type User struct {
// Portal Role Level
PortalRole string `json:"PortalRole,omitempty"`
// Stable machine principal key; empty for legacy human principals
PrincipalKey string `json:"PrincipalKey,omitempty"`
// Principal type, currently service or legacy_user
PrincipalType string `json:"PrincipalType,omitempty"`
// Profile
ProfileID string `json:"ProfileID,omitempty"`
@ -157,6 +163,9 @@ type User struct {
// Admin Info Emails
ReceivesAdminInfoEmails bool `json:"ReceivesAdminInfoEmails,omitempty"`
// Explicit mesh authorization scopes; never contains credentials
Scopes []string `json:"Scopes"`
// Email Sender Address
SenderEmail string `json:"SenderEmail,omitempty"`

View File

@ -10,7 +10,6 @@ import (
//
// When called, ChechAPIUser assumes there is an "auth" service account in the
// app configuration file.
//
func CheckAPIUser(token *string) (*User, error) {
sugar.Debug("app.CheckAPIUser: 📥")
if authUser == nil {
@ -27,7 +26,7 @@ func CheckAPIUser(token *string) (*User, error) {
params.Apikey = token
response, err := authClient.User.GetUsers(params, authUser.Auth)
if err != nil {
sugar.Warnf("app.CheckAPIUser: ❗ Access attempt with invalid API key: %s", *token)
sugar.Warn("app.CheckAPIUser: ❗ access attempt with invalid API key")
return nil, err
}
var obj *auth_models.User

View File

@ -69,6 +69,7 @@ type ServiceAccount struct {
// Configuration defines the config struct
type Configuration struct {
AppName string `mapstructure:"app_name,omitempty"`
AuthMethod string `mapstructure:"auth_method,omitempty"`
BackendID string `mapstructure:"backend_id,omitempty"`
BuildEnv string `mapstructure:"build_env,omitempty"`
CacheSizes map[string]CacheSize `mapstructure:"cache_sizes,omitempty"`
@ -98,6 +99,11 @@ type Configuration struct {
Workers map[string]Worker `mapstructure:"workers,omitempty"`
}
// GetAuthMethod returns the configured authentication-authority mode.
func GetAuthMethod() string {
return config.AuthMethod
}
// GetCacheSize returns the named cache size
func GetCacheSize(name string) int64 {
obj, ok := config.CacheSizes[name]

View File

@ -76,12 +76,15 @@ func MarshalAuthUserToSwagger(obj *auth_models.User) *User {
OutOfOfficeMessage: obj.OutOfOfficeMessage,
Phone: obj.Phone,
PortalRole: obj.PortalRole,
PrincipalKey: obj.PrincipalKey,
PrincipalType: obj.PrincipalType,
ProfileID: obj.ProfileID,
ReceivesAdminEmails: obj.ReceivesAdminEmails,
ReceivesAdminInfoEmails: obj.ReceivesAdminInfoEmails,
SenderEmail: obj.SenderEmail,
SenderName: obj.SenderName,
Signature: obj.Signature,
Scopes: append([]string(nil), obj.Scopes...),
SmallPhotoURL: obj.SmallPhotoURL,
StartOfDay: obj.StartOfDay,
ExternalAccount: obj.ExternalAccount,

View File

@ -57,6 +57,8 @@ type User struct {
OutOfOfficeMessage string
Phone string
PortalRole string
PrincipalKey string
PrincipalType string
ProfileID string
ReceivesAdminEmails bool
ReceivesAdminInfoEmails bool
@ -64,6 +66,7 @@ type User struct {
SenderEmail string
SenderName string
Signature string
Scopes []string
SmallPhotoURL string
StartOfDay string
ExternalAccount string
@ -76,3 +79,22 @@ type User struct {
UserRoleID string
UserType string
}
// HasScope reports whether a native service principal has an exact mesh
// authorization scope. Legacy user principals have no scopes.
func (u *User) HasScope(scope string) bool {
if u == nil || u.PrincipalType != "service" {
return false
}
for _, granted := range u.Scopes {
if granted == scope {
return true
}
}
return false
}
// IsServicePrincipal matches stable machine identity, never a bearer secret.
func (u *User) IsServicePrincipal(principalKey string) bool {
return u != nil && u.PrincipalType == "service" && u.PrincipalKey == principalKey
}

27
app/user_test.go Normal file
View File

@ -0,0 +1,27 @@
package app
import "testing"
func TestNativeServicePrincipalScope(t *testing.T) {
principal := &User{
PrincipalType: "service",
PrincipalKey: "cache",
Scopes: []string{"auth:introspect", "crm:account-contact:load"},
}
if !principal.IsServicePrincipal("cache") {
t.Fatal("expected stable service-principal match")
}
if !principal.HasScope("crm:account-contact:load") {
t.Fatal("expected exact scope grant")
}
if principal.HasScope("crm:account:write") {
t.Fatal("unexpected scope grant")
}
}
func TestLegacyUserCannotClaimServiceScope(t *testing.T) {
legacy := &User{APIKey: "legacy", Scopes: []string{"crm:account-contact:load"}}
if legacy.IsServicePrincipal("cache") || legacy.HasScope("crm:account-contact:load") {
t.Fatal("legacy user must not satisfy native service authorization")
}
}

View File

@ -115,8 +115,9 @@ cmd_install_swagger() {
bin="$(swagger_bin)"
[[ -x "$bin" ]] || die "go install reported success but ${bin} is not executable"
local got
got="$("$bin" version 2>&1 | head -1)"
local got version_output
version_output="$("$bin" version 2>&1)"
got="${version_output%%$'\n'*}"
log "swagger version: ${got}"
case "$got" in
*"${SWAGGER_VERSION#v}"*) ;;

View File

@ -1,6 +1,6 @@
swagger: "2.0"
info:
version: 0.3.0
version: 0.4.0
title: "auth"
description: "Authentication Microservice"
termsOfService: "https://salesforcedevops.net/terms/"
@ -272,6 +272,12 @@ definitions:
PortalRole:
description: Portal Role Level
type: string
PrincipalKey:
description: Stable machine principal key; empty for legacy human principals
type: string
PrincipalType:
description: Principal type, currently service or legacy_user
type: string
ProfileID:
description: Profile
type: string
@ -290,6 +296,11 @@ definitions:
Signature:
description: Email Signature
type: string
Scopes:
description: Explicit mesh authorization scopes; never contains credentials
items:
type: string
type: array
SmallPhotoURL:
description: Small Photo URL
type: string

View File

@ -1,6 +1,6 @@
swagger: "2.0"
info:
version: 0.3.0
version: 0.4.0
title: "auth"
description: "Authentication Microservice"
termsOfService: "https://salesforcedevops.net/terms/"
@ -272,6 +272,12 @@ definitions:
PortalRole:
description: Portal Role Level
type: string
PrincipalKey:
description: Stable machine principal key; empty for legacy human principals
type: string
PrincipalType:
description: Principal type, currently service or legacy_user
type: string
ProfileID:
description: Profile
type: string
@ -290,6 +296,11 @@ definitions:
Signature:
description: Email Signature
type: string
Scopes:
description: Explicit mesh authorization scopes; never contains credentials
items:
type: string
type: array
SmallPhotoURL:
description: Small Photo URL
type: string