diff --git a/cmd/tsidp/tsidp.go b/cmd/tsidp/tsidp.go index 15f7e6e9c..e2b777fa1 100644 --- a/cmd/tsidp/tsidp.go +++ b/cmd/tsidp/tsidp.go @@ -543,14 +543,14 @@ type userInfo struct { } type capRule struct { - IncludeInUserInfo bool `json:"includeInUserInfo"` - ExtraClaims map[string]interface{} `json:"extraClaims,omitempty"` // list of features peer is allowed to edit + IncludeInUserInfo bool `json:"includeInUserInfo"` + ExtraClaims map[string]any `json:"extraClaims,omitempty"` // list of features peer is allowed to edit } // flattenExtraClaims merges all ExtraClaims from a slice of capRule into a single map. // It deduplicates values for each claim and preserves the original input type: -// scalar values remain scalars, and slices are returned as deduplicated []interface{} slices. -func flattenExtraClaims(rules []capRule) map[string]interface{} { +// scalar values remain scalars, and slices are returned as deduplicated []any slices. +func flattenExtraClaims(rules []capRule) map[string]any { // sets stores deduplicated stringified values for each claim key. sets := make(map[string]map[string]struct{}) @@ -561,7 +561,7 @@ func flattenExtraClaims(rules []capRule) map[string]interface{} { for claim, raw := range rule.ExtraClaims { // Track whether the claim was provided as a slice switch raw.(type) { - case []string, []interface{}: + case []string, []any: isSlice[claim] = true default: // Only mark as scalar if this is the first time we've seen this claim @@ -576,11 +576,11 @@ func flattenExtraClaims(rules []capRule) map[string]interface{} { } // Build final result: either scalar or slice depending on original type - result := make(map[string]interface{}) + result := make(map[string]any) for claim, valSet := range sets { if isSlice[claim] { - // Claim was provided as a slice: output as []interface{} - var vals []interface{} + // Claim was provided as a slice: output as []any + var vals []any for val := range valSet { vals = append(vals, val) } @@ -600,7 +600,7 @@ func flattenExtraClaims(rules []capRule) map[string]interface{} { // addClaimValue adds a claim value to the deduplication set for a given claim key. // It accepts scalars (string, int, float64), slices of strings or interfaces, // and recursively handles nested slices. Unsupported types are ignored with a log message. -func addClaimValue(sets map[string]map[string]struct{}, claim string, val interface{}) { +func addClaimValue(sets map[string]map[string]struct{}, claim string, val any) { switch v := val.(type) { case string, float64, int, int64: // Ensure the claim set is initialized @@ -620,7 +620,7 @@ func addClaimValue(sets map[string]map[string]struct{}, claim string, val interf sets[claim][s] = struct{}{} } - case []interface{}: + case []any: // Recursively handle each item in the slice for _, item := range v { addClaimValue(sets, claim, item) @@ -633,7 +633,7 @@ func addClaimValue(sets map[string]map[string]struct{}, claim string, val interf } // withExtraClaims merges flattened extra claims from a list of capRule into the provided struct v, -// returning a map[string]interface{} that combines both sources. +// returning a map[string]any that combines both sources. // // v is any struct whose fields represent static claims; it is first marshaled to JSON, then unmarshalled into a generic map. // rules is a slice of capRule objects that may define additional (extra) claims to merge. @@ -643,7 +643,7 @@ func addClaimValue(sets map[string]map[string]struct{}, claim string, val interf // If an extra claim attempts to overwrite a protected claim, an error is returned. // // Returns the merged claims map or an error if any protected claim is violated or JSON (un)marshaling fails. -func withExtraClaims(v any, rules []capRule) (map[string]interface{}, error) { +func withExtraClaims(v any, rules []capRule) (map[string]any, error) { // Marshal the static struct data, err := json.Marshal(v) if err != nil { @@ -651,7 +651,7 @@ func withExtraClaims(v any, rules []capRule) (map[string]interface{}, error) { } // Unmarshal into a generic map - var claimMap map[string]interface{} + var claimMap map[string]any if err := json.Unmarshal(data, &claimMap); err != nil { return nil, err } diff --git a/cmd/tsidp/tsidp_test.go b/cmd/tsidp/tsidp_test.go index f6122708a..76a118991 100644 --- a/cmd/tsidp/tsidp_test.go +++ b/cmd/tsidp/tsidp_test.go @@ -7,8 +7,6 @@ import ( "crypto/rsa" "encoding/json" "fmt" - "gopkg.in/square/go-jose.v2" - "gopkg.in/square/go-jose.v2/jwt" "io" "log" "net/http" @@ -18,29 +16,32 @@ import ( "reflect" "sort" "strings" + "testing" + "time" + + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/views" - "testing" - "time" ) -// normalizeMap recursively sorts []interface{} values in a map[string]interface{} -func normalizeMap(t *testing.T, m map[string]interface{}) map[string]interface{} { +// normalizeMap recursively sorts []any values in a map[string]any +func normalizeMap(t *testing.T, m map[string]any) map[string]any { t.Helper() - normalized := make(map[string]interface{}, len(m)) + normalized := make(map[string]any, len(m)) for k, v := range m { switch val := v.(type) { - case []interface{}: + case []any: sorted := make([]string, len(val)) for i, item := range val { sorted[i] = fmt.Sprintf("%v", item) // convert everything to string for sorting } sort.Strings(sorted) - // convert back to []interface{} - sortedIface := make([]interface{}, len(sorted)) + // convert back to []any + sortedIface := make([]any, len(sorted)) for i, s := range sorted { sortedIface[i] = s } @@ -101,26 +102,26 @@ func TestFlattenExtraClaims(t *testing.T) { tests := []struct { name string input []capRule - expected map[string]interface{} + expected map[string]any }{ { name: "empty extra claims", input: []capRule{ - {ExtraClaims: map[string]interface{}{}}, + {ExtraClaims: map[string]any{}}, }, - expected: map[string]interface{}{}, + expected: map[string]any{}, }, { name: "string and number values", input: []capRule{ { - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "featureA": "read", "featureB": 42, }, }, }, - expected: map[string]interface{}{ + expected: map[string]any{ "featureA": "read", "featureB": "42", }, @@ -129,95 +130,95 @@ func TestFlattenExtraClaims(t *testing.T) { name: "slice of strings and ints", input: []capRule{ { - ExtraClaims: map[string]interface{}{ - "roles": []interface{}{"admin", "user", 1}, + ExtraClaims: map[string]any{ + "roles": []any{"admin", "user", 1}, }, }, }, - expected: map[string]interface{}{ - "roles": []interface{}{"admin", "user", "1"}, + expected: map[string]any{ + "roles": []any{"admin", "user", "1"}, }, }, { name: "duplicate values deduplicated (slice input)", input: []capRule{ { - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": []string{"bar", "baz"}, }, }, { - ExtraClaims: map[string]interface{}{ - "foo": []interface{}{"bar", "qux"}, + ExtraClaims: map[string]any{ + "foo": []any{"bar", "qux"}, }, }, }, - expected: map[string]interface{}{ - "foo": []interface{}{"bar", "baz", "qux"}, + expected: map[string]any{ + "foo": []any{"bar", "baz", "qux"}, }, }, { name: "ignore unsupported map type, keep valid scalar", input: []capRule{ { - ExtraClaims: map[string]interface{}{ - "invalid": map[string]interface{}{"bad": "yes"}, + ExtraClaims: map[string]any{ + "invalid": map[string]any{"bad": "yes"}, "valid": "ok", }, }, }, - expected: map[string]interface{}{ + expected: map[string]any{ "valid": "ok", }, }, { name: "scalar first, slice second", input: []capRule{ - {ExtraClaims: map[string]interface{}{"foo": "bar"}}, - {ExtraClaims: map[string]interface{}{"foo": []interface{}{"baz"}}}, + {ExtraClaims: map[string]any{"foo": "bar"}}, + {ExtraClaims: map[string]any{"foo": []any{"baz"}}}, }, - expected: map[string]interface{}{ - "foo": []interface{}{"bar", "baz"}, // since first was scalar, second being a slice forces slice output + expected: map[string]any{ + "foo": []any{"bar", "baz"}, // since first was scalar, second being a slice forces slice output }, }, { name: "conflicting scalar and unsupported map", input: []capRule{ - {ExtraClaims: map[string]interface{}{"foo": "bar"}}, - {ExtraClaims: map[string]interface{}{"foo": map[string]interface{}{"bad": "entry"}}}, + {ExtraClaims: map[string]any{"foo": "bar"}}, + {ExtraClaims: map[string]any{"foo": map[string]any{"bad": "entry"}}}, }, - expected: map[string]interface{}{ + expected: map[string]any{ "foo": "bar", // map should be ignored }, }, { name: "multiple slices with overlap", input: []capRule{ - {ExtraClaims: map[string]interface{}{"roles": []interface{}{"admin", "user"}}}, - {ExtraClaims: map[string]interface{}{"roles": []interface{}{"admin", "guest"}}}, + {ExtraClaims: map[string]any{"roles": []any{"admin", "user"}}}, + {ExtraClaims: map[string]any{"roles": []any{"admin", "guest"}}}, }, - expected: map[string]interface{}{ - "roles": []interface{}{"admin", "user", "guest"}, + expected: map[string]any{ + "roles": []any{"admin", "user", "guest"}, }, }, { name: "slice with unsupported values", input: []capRule{ - {ExtraClaims: map[string]interface{}{ - "mixed": []interface{}{"ok", 42, map[string]string{"oops": "fail"}}, + {ExtraClaims: map[string]any{ + "mixed": []any{"ok", 42, map[string]string{"oops": "fail"}}, }}, }, - expected: map[string]interface{}{ - "mixed": []interface{}{"ok", "42"}, // map is ignored + expected: map[string]any{ + "mixed": []any{"ok", "42"}, // map is ignored }, }, { name: "duplicate scalar value", input: []capRule{ - {ExtraClaims: map[string]interface{}{"env": "prod"}}, - {ExtraClaims: map[string]interface{}{"env": "prod"}}, + {ExtraClaims: map[string]any{"env": "prod"}}, + {ExtraClaims: map[string]any{"env": "prod"}}, }, - expected: map[string]interface{}{ + expected: map[string]any{ "env": "prod", // not converted to slice }, }, @@ -242,7 +243,7 @@ func TestExtraClaims(t *testing.T) { name string claim tailscaleClaims extraClaims []capRule - expected map[string]interface{} + expected map[string]any expectError bool }{ { @@ -261,12 +262,12 @@ func TestExtraClaims(t *testing.T) { }, extraClaims: []capRule{ { - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": []string{"bar"}, }, }, }, - expected: map[string]interface{}{ + expected: map[string]any{ "nonce": "foobar", "key": "nodekey:0000000000000000000000000000000000000000000000000000000000000000", "addresses": nil, @@ -275,7 +276,7 @@ func TestExtraClaims(t *testing.T) { "tailnet": "test.ts.net", "email": "test@example.com", "username": "test", - "foo": []interface{}{"bar"}, + "foo": []any{"bar"}, }, }, { @@ -294,17 +295,17 @@ func TestExtraClaims(t *testing.T) { }, extraClaims: []capRule{ { - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": []string{"bar"}, }, }, { - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": []string{"foobar"}, }, }, }, - expected: map[string]interface{}{ + expected: map[string]any{ "nonce": "foobar", "key": "nodekey:0000000000000000000000000000000000000000000000000000000000000000", "addresses": nil, @@ -313,7 +314,7 @@ func TestExtraClaims(t *testing.T) { "tailnet": "test.ts.net", "email": "test@example.com", "username": "test", - "foo": []interface{}{"foobar", "bar"}, + "foo": []any{"foobar", "bar"}, }, }, { @@ -332,17 +333,17 @@ func TestExtraClaims(t *testing.T) { }, extraClaims: []capRule{ { - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": []string{"bar"}, }, }, { - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "bar": []string{"foo"}, }, }, }, - expected: map[string]interface{}{ + expected: map[string]any{ "nonce": "foobar", "key": "nodekey:0000000000000000000000000000000000000000000000000000000000000000", "addresses": nil, @@ -351,8 +352,8 @@ func TestExtraClaims(t *testing.T) { "tailnet": "test.ts.net", "email": "test@example.com", "username": "test", - "foo": []interface{}{"bar"}, - "bar": []interface{}{"foo"}, + "foo": []any{"bar"}, + "bar": []any{"foo"}, }, }, { @@ -371,12 +372,12 @@ func TestExtraClaims(t *testing.T) { }, extraClaims: []capRule{ { - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "username": "foobar", }, }, }, - expected: map[string]interface{}{ + expected: map[string]any{ "nonce": "foobar", "key": "nodekey:0000000000000000000000000000000000000000000000000000000000000000", "addresses": nil, @@ -402,8 +403,8 @@ func TestExtraClaims(t *testing.T) { UserID: 0, UserName: "test", }, - extraClaims: []capRule{{ExtraClaims: map[string]interface{}{}}}, - expected: map[string]interface{}{ + extraClaims: []capRule{{ExtraClaims: map[string]any{}}}, + expected: map[string]any{ "nonce": "foobar", "key": "nodekey:0000000000000000000000000000000000000000000000000000000000000000", "addresses": nil, @@ -427,13 +428,13 @@ func TestExtraClaims(t *testing.T) { return // just as expected } - // Marshal to JSON then unmarshal back to map[string]interface{} + // Marshal to JSON then unmarshal back to map[string]any gotClaims, err := json.Marshal(claims) if err != nil { t.Errorf("json.Marshal(claims) error = %v", err) } - var gotClaimsMap map[string]interface{} + var gotClaimsMap map[string]any if err := json.Unmarshal(gotClaims, &gotClaimsMap); err != nil { t.Fatalf("json.Unmarshal(gotClaims) error = %v", err) } @@ -459,7 +460,7 @@ func TestServeToken(t *testing.T) { redirectURI string remoteAddr string expectError bool - expected map[string]interface{} + expected map[string]any }{ { name: "GET not allowed", @@ -516,13 +517,13 @@ func TestServeToken(t *testing.T) { tailcfg.PeerCapabilityTsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": "bar", }, }), }, }, - expected: map[string]interface{}{ + expected: map[string]any{ "foo": "bar", }, }, @@ -536,7 +537,7 @@ func TestServeToken(t *testing.T) { tailcfg.PeerCapabilityTsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "sub": "should-not-overwrite", }, }), @@ -623,7 +624,7 @@ func TestServeToken(t *testing.T) { t.Fatalf("failed to parse ID token: %v", err) } - out := make(map[string]interface{}) + out := make(map[string]any) if err := tok.Claims(oidcTestingPublicKey(t), &out); err != nil { t.Fatalf("failed to extract claims: %v", err) } @@ -647,7 +648,7 @@ func TestExtraUserInfo(t *testing.T) { name string caps tailcfg.PeerCapMap tokenValidTill time.Time - expected map[string]interface{} + expected map[string]any expectError bool }{ { @@ -657,14 +658,14 @@ func TestExtraUserInfo(t *testing.T) { tailcfg.PeerCapabilityTsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": []string{"bar"}, }, }), }, }, - expected: map[string]interface{}{ - "foo": []interface{}{"bar"}, + expected: map[string]any{ + "foo": []any{"bar"}, }, }, { @@ -674,14 +675,14 @@ func TestExtraUserInfo(t *testing.T) { tailcfg.PeerCapabilityTsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": []string{"bar", "foobar"}, }, }), }, }, - expected: map[string]interface{}{ - "foo": []interface{}{"bar", "foobar"}, + expected: map[string]any{ + "foo": []any{"bar", "foobar"}, }, }, { @@ -691,14 +692,14 @@ func TestExtraUserInfo(t *testing.T) { tailcfg.PeerCapabilityTsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": "bar", "bar": "foo", }, }), }, }, - expected: map[string]interface{}{ + expected: map[string]any{ "foo": "bar", "bar": "foo", }, @@ -707,7 +708,7 @@ func TestExtraUserInfo(t *testing.T) { name: "empty extra claims", caps: tailcfg.PeerCapMap{}, tokenValidTill: time.Now().Add(1 * time.Minute), - expected: map[string]interface{}{}, + expected: map[string]any{}, }, { name: "attempt to overwrite protected claim", @@ -716,7 +717,7 @@ func TestExtraUserInfo(t *testing.T) { tailcfg.PeerCapabilityTsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "sub": "should-not-overwrite", "foo": "ok", }, @@ -732,19 +733,19 @@ func TestExtraUserInfo(t *testing.T) { tailcfg.PeerCapabilityTsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: false, - ExtraClaims: map[string]interface{}{ + ExtraClaims: map[string]any{ "foo": "ok", }, }), }, }, - expected: map[string]interface{}{}, + expected: map[string]any{}, }, { name: "expired token", caps: tailcfg.PeerCapMap{}, tokenValidTill: time.Now().Add(-1 * time.Minute), - expected: map[string]interface{}{}, + expected: map[string]any{}, expectError: true, }, } @@ -802,7 +803,7 @@ func TestExtraUserInfo(t *testing.T) { t.Fatalf("expected 200 OK, got %d: %s", rr.Code, rr.Body.String()) } - var resp map[string]interface{} + var resp map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse JSON response: %v", err) }