Skip to content

Commit

Permalink
Merge pull request #4 from fabiofenoglio/idp-groups-compatibility
Browse files Browse the repository at this point in the history
Implement compatibility with different IDP group formats without impacting the current API
  • Loading branch information
fabiofenoglio authored Dec 5, 2022
2 parents 3d1a0b4 + 6e17304 commit 4588c83
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 4 deletions.
126 changes: 122 additions & 4 deletions cloudflare_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,12 @@ func (s *cloudflareAccessClientImpl) BuildPrincipal(ctx context.Context, raw str
// extractGroups makes an HTTP call to a specific endpoint in order to extract the list
// of groups that the user belongs to.
func (s *cloudflareAccessClientImpl) fetchIdentity(ctx context.Context, raw string, token *oidc.IDToken) (*CloudflareIdentity, error) {
// fetching from https://<teamDomain>.cloudflareaccess.com/cdn-cgi/access/get-identity
if s.config.identityFetcher != nil {
return s.config.identityFetcher(ctx, raw, token)
}

var identityResponse CloudflareIdentity
// fetching from https://<teamDomain>.cloudflareaccess.com/cdn-cgi/access/get-identity
var identityResponse cloudflareIdentityResponse

client := resty.New()

Expand Down Expand Up @@ -188,10 +188,128 @@ func (s *cloudflareAccessClientImpl) fetchIdentity(ctx context.Context, raw stri
return nil, fmt.Errorf("got HTTP server error reading user groups: %v %v", resp.StatusCode(), resp.Status())
}

return &identityResponse, nil
mappedGroups, err := mapIDPGroupsToCloudflareGroups(identityResponse.RawGroups)
if err != nil {
return nil, fmt.Errorf("error building access groups from IDP groups: %w", err)
}

return &CloudflareIdentity{
Id: identityResponse.Id,
Name: identityResponse.Name,
Email: identityResponse.Email,
UserUUID: identityResponse.UserUUID,
AccountId: identityResponse.AccountId,
IP: identityResponse.IP,
AuthStatus: identityResponse.AuthStatus,
CommonName: identityResponse.CommonName,
ServiceTokenId: identityResponse.ServiceTokenId,
ServiceTokenStatus: identityResponse.ServiceTokenStatus,
IsWarp: identityResponse.IsWarp,
IsGateway: identityResponse.IsGateway,
Version: identityResponse.Version,
DeviceSessions: identityResponse.DeviceSessions,
IssuedAt: identityResponse.IssuedAt,
Idp: identityResponse.Idp,
Geographical: identityResponse.Geographical,
Groups: mappedGroups,
}, nil
}

func mapIDPGroupsToCloudflareGroups(rawGroupsEntry interface{}) ([]CloudflareIdentityGroup, error) {
if rawGroupsEntry == nil {
return []CloudflareIdentityGroup{}, nil
}

rawGroups, isArray := rawGroupsEntry.([]interface{})
if !isArray {
return nil, errors.New("groups entry is not an array")
}

out := make([]CloudflareIdentityGroup, 0, len(rawGroups))

for _, rawGroup := range rawGroups {
mapped, err := mapIDPGroupToCloudflareGroup(rawGroup)
if err != nil {
return nil, err
}
out = append(out, mapped)
}

return out, nil
}

func mapIDPGroupToCloudflareGroup(rawGroup interface{}) (CloudflareIdentityGroup, error) {

if asString, ok := rawGroup.(string); ok {
return CloudflareIdentityGroup{
Id: asString,
Name: asString,
Email: "",
}, nil
}

if asMap, ok := rawGroup.(map[string]interface{}); ok {
mapped := CloudflareIdentityGroup{}

if v, hasField := asMap["id"]; hasField {
if vAsString, fieldIsString := v.(string); fieldIsString && vAsString != "" {
mapped.Id = vAsString
}
}
if v, hasField := asMap["name"]; hasField {
if vAsString, fieldIsString := v.(string); fieldIsString && vAsString != "" {
mapped.Name = vAsString
}
}
if v, hasField := asMap["email"]; hasField {
if vAsString, fieldIsString := v.(string); fieldIsString && vAsString != "" {
mapped.Email = vAsString
}
}

if mapped.Id != "" || mapped.Email != "" {
if mapped.Name == "" {
if mapped.Email != "" {
mapped.Name = mapped.Email
} else if mapped.Id != "" {
mapped.Name = mapped.Id
}
}
if mapped.Id == "" {
if mapped.Email != "" {
mapped.Id = mapped.Email
}
}
return mapped, nil
}
}

return CloudflareIdentityGroup{}, errors.New("unknown format for IDP group entry")
}

// cloudflareIdentityResponse is the REST model for the response holding the user identity
type cloudflareIdentityResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
UserUUID string `json:"user_uuid"`
AccountId string `json:"account_id"`
IP string `json:"ip"`
AuthStatus string `json:"auth_status"`
CommonName string `json:"common_name"`
ServiceTokenId string `json:"service_token_id"`
ServiceTokenStatus bool `json:"service_token_status"`
IsWarp bool `json:"is_warp"`
IsGateway bool `json:"is_gateway"`
Version int `json:"version"`
DeviceSessions map[string]interface{} `json:"device_sessions"`
IssuedAt int `json:"iat"`
Idp *CloudflareIdentityProvider `json:"idp"`
Geographical *CloudflareIdentityGeographical `json:"geo"`
RawGroups interface{} `json:"groups"`
}

// CloudflareIdentity is the REST model for the response holding the user identity
// CloudflareIdentity is the model for the user identity
type CloudflareIdentity struct {
Id string `json:"id"`
Name string `json:"name"`
Expand Down
89 changes: 89 additions & 0 deletions cloudflare_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package gincloudflareaccess

import (
"encoding/json"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_mapIDPGroupsToCloudflareGroups(t *testing.T) {
expectedErrBadFormat := "groups entry is not an array"
expectedErrBadIDPFormat := "unknown format for IDP group entry"

tests := []struct {
input string
want []CloudflareIdentityGroup
wantErr string
}{
{input: `null`, want: []CloudflareIdentityGroup{}},
{input: `[]`, want: []CloudflareIdentityGroup{}},
{input: `{}`, wantErr: expectedErrBadFormat},
{input: `123`, wantErr: expectedErrBadFormat},
{input: `"aaa"`, wantErr: expectedErrBadFormat},
{input: `[null]`, wantErr: expectedErrBadIDPFormat},
{input: `[123]`, wantErr: expectedErrBadIDPFormat},
{input: `[[]]`, wantErr: expectedErrBadIDPFormat},
{input: `[{}]`, wantErr: expectedErrBadIDPFormat},
{input: `[{"name": "just a name"}]`, wantErr: expectedErrBadIDPFormat},
{input: `[{"another_field": 42}]`, wantErr: expectedErrBadIDPFormat},
{
input: `["group 1"]`,
want: []CloudflareIdentityGroup{
{Id: "group 1", Name: "group 1"},
},
},
{
input: `[{"id": "g1"}]`,
want: []CloudflareIdentityGroup{
{Id: "g1", Name: "g1"},
},
},
{
input: `[{"email": "[email protected]"}]`,
want: []CloudflareIdentityGroup{
{Id: "[email protected]", Name: "[email protected]", Email: "[email protected]"},
},
},
{
input: `[
"G0",
{"id": "G1"},
{"email": "[email protected]"},
"G3",
{"id": "G4", "name": "group 4"},
{"id": "G5", "name": "group 5", "email": "[email protected]"},
{"id": "G6", "email": "[email protected]"}
]`,
want: []CloudflareIdentityGroup{
{Id: "G0", Name: "G0"},
{Id: "G1", Name: "G1"},
{Id: "[email protected]", Name: "[email protected]", Email: "[email protected]"},
{Id: "G3", Name: "G3"},
{Id: "G4", Name: "group 4"},
{Id: "G5", Name: "group 5", Email: "[email protected]"},
{Id: "G6", Name: "[email protected]", Email: "[email protected]"},
},
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("test_case_%d", i), func(t *testing.T) {
var unmarshalled interface{}

err := json.Unmarshal([]byte(tt.input), &unmarshalled)
assert.NoError(t, err)

got, err := mapIDPGroupsToCloudflareGroups(unmarshalled)

if tt.wantErr != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
assert.NoError(t, err)
}

assert.Equalf(t, tt.want, got, "mapIDPGroupsToCloudflareGroups(%v)", tt.input)
})
}
}

0 comments on commit 4588c83

Please sign in to comment.