Path: blob/main/components/public-api-server/pkg/apiv1/oidc_test.go
2499 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package apiv156import (7"context"8"net/http"9"net/http/httptest"10"sort"11"strings"12"testing"1314"gorm.io/gorm"1516db "github.com/gitpod-io/gitpod/components/gitpod-db/go"17"github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest"18"github.com/go-chi/chi/v5"19"github.com/go-chi/chi/v5/middleware"20"github.com/google/uuid"2122connect "github.com/bufbuild/connect-go"23"github.com/gitpod-io/gitpod/common-go/experiments"24"github.com/gitpod-io/gitpod/common-go/experiments/experimentstest"25"github.com/gitpod-io/gitpod/components/public-api/go/config"26v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"27"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"28protocol "github.com/gitpod-io/gitpod/gitpod-protocol"29"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"30"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"31"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"32"github.com/golang/mock/gomock"33"github.com/stretchr/testify/require"34)3536var (37withOIDCFeatureDisabled = &experimentstest.Client{38BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool {39return false40},41}42withOIDCFeatureEnabled = &experimentstest.Client{43BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool {44return experiment == experiments.OIDCServiceEnabledFlag45},46}4748user = newUser(&protocol.User{})49organizationID = uuid.New()50)5152func TestOIDCService_CreateClientConfig_FeatureFlagDisabled(t *testing.T) {5354t.Run("returns unauthorized", func(t *testing.T) {55_, client, _ := setupOIDCService(t, withOIDCFeatureDisabled)56issuer := newFakeIdP(t, true)5758_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{59Config: &v1.OIDCClientConfig{60OrganizationId: organizationID.String(),61OidcConfig: &v1.OIDCConfig{62Issuer: issuer,63},64},65}))66require.Error(t, err)67require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))68})69}7071func TestOIDCService_CreateClientConfig_FeatureFlagEnabled(t *testing.T) {72t.Run("returns invalid argument when no organisation specified", func(t *testing.T) {73_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)74issuer := newFakeIdP(t, true)7576config := &v1.OIDCClientConfig{77OidcConfig: &v1.OIDCConfig{Issuer: issuer},78Oauth2Config: &v1.OAuth2Config{ClientId: "test-id", ClientSecret: "test-secret"},79}80_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{81Config: config,82}))83require.Error(t, err)84require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))85})8687t.Run("returns invalid argument when organisation id is not a uuid", func(t *testing.T) {88_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)89issuer := newFakeIdP(t, true)9091config := &v1.OIDCClientConfig{92OrganizationId: "some-random-id",93OidcConfig: &v1.OIDCConfig{Issuer: issuer},94Oauth2Config: &v1.OAuth2Config{ClientId: "test-id", ClientSecret: "test-secret"},95}96_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{97Config: config,98}))99require.Error(t, err)100require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))101})102103t.Run("returns permission denied when user is not org owner", func(t *testing.T) {104_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)105issuer := newFakeIdP(t, true)106107anotherOrg := uuid.New()108dbtest.CreateTeamMembership(t, dbConn, db.OrganizationMembership{109OrganizationID: anotherOrg,110UserID: uuid.MustParse(user.ID),111Role: db.OrganizationMembershipRole_Member,112})113114config := &v1.OIDCClientConfig{115OrganizationId: anotherOrg.String(),116OidcConfig: &v1.OIDCConfig{Issuer: issuer},117Oauth2Config: &v1.OAuth2Config{ClientId: "test-id", ClientSecret: "test-secret"},118}119_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{120Config: config,121}))122require.Error(t, err)123require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))124})125126t.Run("returns invalid argument when issuer is not valid URL", func(t *testing.T) {127_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)128129config := &v1.OIDCClientConfig{130OrganizationId: organizationID.String(),131OidcConfig: &v1.OIDCConfig{Issuer: "random thing which is not url"},132Oauth2Config: &v1.OAuth2Config{ClientId: "test-id", ClientSecret: "test-secret"},133}134_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{135Config: config,136}))137require.Error(t, err)138require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))139})140141t.Run("returns invalid argument when issuer is not reachable", func(t *testing.T) {142_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)143144config := &v1.OIDCClientConfig{145OrganizationId: organizationID.String(),146OidcConfig: &v1.OIDCConfig{Issuer: "https://this-host-is-not-reachable"},147Oauth2Config: &v1.OAuth2Config{ClientId: "test-id", ClientSecret: "test-secret"},148}149_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{150Config: config,151}))152require.Error(t, err)153require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))154})155156t.Run("returns invalid argument when issuer does not provide discovery", func(t *testing.T) {157_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)158issuer := newFakeIdP(t, false)159160config := &v1.OIDCClientConfig{161OrganizationId: organizationID.String(),162OidcConfig: &v1.OIDCConfig{Issuer: issuer},163Oauth2Config: &v1.OAuth2Config{ClientId: "test-id", ClientSecret: "test-secret"},164}165_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{166Config: config,167}))168require.Error(t, err)169require.Contains(t, err.Error(), "needs to support OIDC Discovery")170require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))171})172173t.Run("creates oidc client config", func(t *testing.T) {174_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)175issuer := newFakeIdP(t, true)176177// Trailing slashes should be removed from the issuer178issuerWithTrailingSlash := issuer + "/"179config := &v1.OIDCClientConfig{180OrganizationId: organizationID.String(),181OidcConfig: &v1.OIDCConfig{Issuer: issuerWithTrailingSlash},182Active: true,183Oauth2Config: &v1.OAuth2Config{184ClientId: "test-id",185ClientSecret: "test-secret",186Scopes: []string{"my-scope"},187},188}189response, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{190Config: config,191}))192require.NoError(t, err)193194requireEqualProto(t, &v1.CreateClientConfigResponse{195Config: &v1.OIDCClientConfig{196Id: response.Msg.Config.Id,197Active: true,198OrganizationId: response.Msg.Config.OrganizationId,199Oauth2Config: &v1.OAuth2Config{200ClientId: config.Oauth2Config.ClientId,201ClientSecret: "REDACTED",202Scopes: []string{"openid", "profile", "email", "my-scope"},203},204OidcConfig: &v1.OIDCConfig{205Issuer: issuer,206},207},208}, response.Msg)209210t.Cleanup(func() {211dbtest.HardDeleteOIDCClientConfigs(t, response.Msg.Config.GetId())212})213214retrieved, err := db.GetOIDCClientConfig(context.Background(), dbConn, uuid.MustParse(response.Msg.Config.Id))215require.NoError(t, err)216require.Equal(t, issuer, retrieved.Issuer, "issuer must not contain trailing slash")217218decrypted, err := retrieved.Data.Decrypt(dbtest.CipherSet(t))219require.NoError(t, err)220require.Equal(t, toDbOIDCSpec(config.Oauth2Config), decrypted)221})222}223224func TestOIDCService_GetClientConfig_WithFeatureFlagDisabled(t *testing.T) {225_, client, _ := setupOIDCService(t, withOIDCFeatureDisabled)226227_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{228Id: uuid.NewString(),229OrganizationId: uuid.NewString(),230}))231require.Error(t, err)232require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))233}234235func TestOIDCService_GetClientConfig_WithFeatureFlagEnabled(t *testing.T) {236237t.Run("invalid argument when config id missing", func(t *testing.T) {238_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)239240_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{}))241require.Error(t, err)242require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))243})244245t.Run("invalid argument when organization id missing", func(t *testing.T) {246_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)247248_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{249Id: uuid.NewString(),250}))251require.Error(t, err)252require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))253})254255t.Run("not found when record does not exist", func(t *testing.T) {256_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)257258_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{259Id: uuid.NewString(),260OrganizationId: uuid.NewString(),261}))262require.Error(t, err)263require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))264})265266t.Run("retrieves record when it exists", func(t *testing.T) {267_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)268issuer := newFakeIdP(t, true)269270created := dbtest.CreateOIDCClientConfigs(t, dbConn, db.OIDCClientConfig{271OrganizationID: organizationID,272Issuer: issuer,273})[0]274275resp, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{276Id: created.ID.String(),277OrganizationId: created.OrganizationID.String(),278}))279require.NoError(t, err)280281converted, err := dbOIDCClientConfigToAPI(created, dbtest.CipherSet(t))282require.NoError(t, err)283284requireEqualProto(t, &v1.GetClientConfigResponse{285Config: converted,286}, resp.Msg)287})288289t.Run("returns permission denied when user is not org owner", func(t *testing.T) {290_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)291292anotherOrg := uuid.New()293dbtest.CreateTeamMembership(t, dbConn, db.OrganizationMembership{294OrganizationID: anotherOrg,295UserID: uuid.MustParse(user.ID),296Role: db.OrganizationMembershipRole_Member,297})298299_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{300Id: uuid.NewString(),301OrganizationId: anotherOrg.String(),302}))303require.Error(t, err)304require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))305})306}307308func TestOIDCService_ListClientConfigs_WithFeatureFlagDisabled(t *testing.T) {309_, client, _ := setupOIDCService(t, withOIDCFeatureDisabled)310311_, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{312OrganizationId: organizationID.String(),313}))314require.Error(t, err)315require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))316}317318func TestOIDCService_ListClientConfigs_WithFeatureFlagEnabled(t *testing.T) {319320t.Run("invalid argument when organization id missing", func(t *testing.T) {321_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)322323_, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{}))324require.Error(t, err)325require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))326})327328t.Run("invalid argument when organization id is invalid", func(t *testing.T) {329_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)330331_, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{332OrganizationId: "some-invalid-id",333}))334require.Error(t, err)335require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))336})337338t.Run("returns permission denied when user is not org owner", func(t *testing.T) {339_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)340341anotherOrg := uuid.New()342dbtest.CreateTeamMembership(t, dbConn, db.OrganizationMembership{343OrganizationID: anotherOrg,344UserID: uuid.MustParse(user.ID),345Role: db.OrganizationMembershipRole_Member,346})347348_, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{349OrganizationId: anotherOrg.String(),350}))351require.Error(t, err)352require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))353})354355t.Run("retrieves configs by organization id", func(t *testing.T) {356_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)357issuer := newFakeIdP(t, true)358359anotherOrgID := uuid.New()360361configs := dbtest.CreateOIDCClientConfigs(t, dbConn,362dbtest.NewOIDCClientConfig(t, db.OIDCClientConfig{363OrganizationID: organizationID,364Issuer: issuer,365}),366dbtest.NewOIDCClientConfig(t, db.OIDCClientConfig{367OrganizationID: organizationID,368Issuer: issuer,369}),370dbtest.NewOIDCClientConfig(t, db.OIDCClientConfig{371OrganizationID: anotherOrgID,372Issuer: issuer,373}),374)375376response, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{377OrganizationId: organizationID.String(),378}))379require.NoError(t, err)380381configA, err := dbOIDCClientConfigToAPI(configs[0], dbtest.CipherSet(t))382require.NoError(t, err)383configB, err := dbOIDCClientConfigToAPI(configs[1], dbtest.CipherSet(t))384require.NoError(t, err)385386expected := []*v1.OIDCClientConfig{387configA,388configB,389}390sort.Slice(expected, func(i, j int) bool {391return strings.Compare(expected[i].Id, expected[j].Id) == -1392393})394395requireEqualProto(t, expected, response.Msg.ClientConfigs)396})397398}399400func TestOIDCService_UpdateClientConfig_WithFeatureFlagDisabled(t *testing.T) {401t.Run("feature flag disabled returns unauthorized", func(t *testing.T) {402_, client, _ := setupOIDCService(t, withOIDCFeatureDisabled)403404_, err := client.UpdateClientConfig(context.Background(), connect.NewRequest(&v1.UpdateClientConfigRequest{405Config: &v1.OIDCClientConfig{406Id: uuid.NewString(),407OrganizationId: organizationID.String(),408},409}))410require.Error(t, err)411require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))412})413414}415416func TestOIDCService_UpdateClientConfig_WithFeatureFlagEnabled(t *testing.T) {417t.Run("non-existent config ID returns not found", func(t *testing.T) {418_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)419420_, err := client.UpdateClientConfig(context.Background(), connect.NewRequest(&v1.UpdateClientConfigRequest{421Config: &v1.OIDCClientConfig{422Id: uuid.New().String(),423OrganizationId: organizationID.String(),424},425}))426require.Error(t, err)427require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))428})429430t.Run("returns permission denied when user is not org owner", func(t *testing.T) {431_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)432433anotherOrg := uuid.New()434dbtest.CreateTeamMembership(t, dbConn, db.OrganizationMembership{435OrganizationID: anotherOrg,436UserID: uuid.MustParse(user.ID),437Role: db.OrganizationMembershipRole_Member,438})439440_, err := client.UpdateClientConfig(context.Background(), connect.NewRequest(&v1.UpdateClientConfigRequest{441Config: &v1.OIDCClientConfig{442Id: uuid.New().String(),443OrganizationId: anotherOrg.String(),444},445}))446require.Error(t, err)447require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))448})449450t.Run("partially applies updates to issuer and scopes", func(t *testing.T) {451_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)452issuer := newFakeIdP(t, true)453issuerNew := newFakeIdP(t, true)454455config := &v1.OIDCClientConfig{456OrganizationId: organizationID.String(),457OidcConfig: &v1.OIDCConfig{458Issuer: issuer,459},460Active: true,461Oauth2Config: &v1.OAuth2Config{462ClientId: "test-id",463ClientSecret: "test-secret",464Scopes: []string{"my-scope"},465},466}467created, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{468Config: config,469}))470require.NoError(t, err)471472_, err = client.UpdateClientConfig(context.Background(), connect.NewRequest(&v1.UpdateClientConfigRequest{473Config: &v1.OIDCClientConfig{474Id: created.Msg.GetConfig().Id,475OrganizationId: organizationID.String(),476OidcConfig: &v1.OIDCConfig{477Issuer: issuerNew + "/", // trailing slash should be removed478},479Oauth2Config: &v1.OAuth2Config{480Scopes: []string{"foo"},481},482},483}))484require.NoError(t, err)485486retrieved, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{487Id: created.Msg.GetConfig().GetId(),488OrganizationId: created.Msg.GetConfig().OrganizationId,489}))490require.NoError(t, err)491492require.Equal(t, config.Active, retrieved.Msg.GetConfig().Active, "unexpected change of `active` flag")493require.Equal(t, issuerNew, retrieved.Msg.GetConfig().OidcConfig.Issuer)494require.Equal(t, []string{"email", "foo", "openid", "profile"}, retrieved.Msg.GetConfig().GetOauth2Config().GetScopes())495496})497}498499func TestOIDCService_DeleteClientConfig_WithFeatureFlagDisabled(t *testing.T) {500t.Run("feature flag disabled returns unauthorized", func(t *testing.T) {501_, client, _ := setupOIDCService(t, withOIDCFeatureDisabled)502503_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{504Id: uuid.NewString(),505OrganizationId: uuid.NewString(),506}))507require.Error(t, err)508require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))509})510511}512513func TestOIDCService_DeleteClientConfig_WithFeatureFlagEnabled(t *testing.T) {514t.Run("invalid argument when ID not specified", func(t *testing.T) {515_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)516517_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{}))518require.Error(t, err)519require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))520})521522t.Run("invalid argument when Organization ID not specified", func(t *testing.T) {523_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)524525_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{526Id: uuid.NewString(),527}))528require.Error(t, err)529require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))530})531532t.Run("not found when record does not exist", func(t *testing.T) {533_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)534535_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{536Id: uuid.NewString(),537OrganizationId: organizationID.String(),538}))539require.Error(t, err)540require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))541})542543t.Run("returns permission denied when user is not org owner", func(t *testing.T) {544_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)545546anotherOrg := uuid.New()547dbtest.CreateTeamMembership(t, dbConn, db.OrganizationMembership{548OrganizationID: anotherOrg,549UserID: uuid.MustParse(user.ID),550Role: db.OrganizationMembershipRole_Member,551})552553_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{554Id: uuid.NewString(),555OrganizationId: anotherOrg.String(),556}))557require.Error(t, err)558require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))559})560561t.Run("deletes record", func(t *testing.T) {562_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)563issuer := newFakeIdP(t, true)564565created := dbtest.CreateOIDCClientConfigs(t, dbConn, db.OIDCClientConfig{566OrganizationID: organizationID,567Issuer: issuer,568})[0]569570resp, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{571Id: created.ID.String(),572OrganizationId: created.OrganizationID.String(),573}))574require.NoError(t, err)575requireEqualProto(t, &v1.DeleteClientConfigResponse{}, resp.Msg)576})577}578579func TestOIDCService_SetClientConfigActivation_WithFeatureFlagDisabled(t *testing.T) {580t.Run("feature flag disabled returns unauthorized", func(t *testing.T) {581_, client, _ := setupOIDCService(t, withOIDCFeatureDisabled)582583_, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{584Id: uuid.NewString(),585OrganizationId: uuid.NewString(),586Activate: true,587}))588require.Error(t, err)589require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))590})591592}593594func TestOIDCService_SetClientConfigActivation_WithFeatureFlagEnabled(t *testing.T) {595t.Run("invalid argument when ID not specified", func(t *testing.T) {596_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)597598_, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{}))599require.Error(t, err)600require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))601})602603t.Run("invalid argument when Organization ID not specified", func(t *testing.T) {604_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)605606_, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{607Id: uuid.NewString(),608}))609require.Error(t, err)610require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))611})612613t.Run("returns permission denied when user is not org owner", func(t *testing.T) {614_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)615616anotherOrg := uuid.New()617dbtest.CreateTeamMembership(t, dbConn, db.OrganizationMembership{618OrganizationID: anotherOrg,619UserID: uuid.MustParse(user.ID),620Role: db.OrganizationMembershipRole_Member,621})622623_, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{624Id: uuid.NewString(),625OrganizationId: anotherOrg.String(),626Activate: true,627}))628require.Error(t, err)629require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))630})631632t.Run("activates record", func(t *testing.T) {633_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)634issuer := newFakeIdP(t, true)635636created := dbtest.CreateOIDCClientConfigs(t, dbConn, db.OIDCClientConfig{637OrganizationID: organizationID,638Issuer: issuer,639Active: false,640Verified: db.BoolPointer(true),641})[0]642643resp, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{644Id: created.ID.String(),645OrganizationId: created.OrganizationID.String(),646Activate: true,647}))648require.NoError(t, err)649requireEqualProto(t, &v1.SetClientConfigActivationResponse{}, resp.Msg)650})651652t.Run("fails to activate unverified record", func(t *testing.T) {653_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)654issuer := newFakeIdP(t, true)655656created := dbtest.CreateOIDCClientConfigs(t, dbConn, db.OIDCClientConfig{657OrganizationID: organizationID,658Issuer: issuer,659Active: false,660Verified: db.BoolPointer(false),661})[0]662663_, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{664Id: created.ID.String(),665OrganizationId: created.OrganizationID.String(),666Activate: true,667}))668require.Error(t, err)669require.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err))670})671672t.Run("activation of record should deactivate others", func(t *testing.T) {673_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)674issuer := newFakeIdP(t, true)675676configs := dbtest.CreateOIDCClientConfigs(t, dbConn, db.OIDCClientConfig{677OrganizationID: organizationID,678Issuer: issuer,679Active: true,680Verified: db.BoolPointer(true),681}, db.OIDCClientConfig{682OrganizationID: organizationID,683Issuer: issuer,684Active: false,685Verified: db.BoolPointer(true),686})687688first := configs[0]689second := configs[1]690691_, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{692Id: second.ID.String(),693OrganizationId: organizationID.String(),694Activate: true,695}))696require.NoError(t, err)697698getFirstConfigResponse, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{699Id: first.ID.String(),700OrganizationId: organizationID.String(),701}))702require.NoError(t, err)703require.Equal(t, false, getFirstConfigResponse.Msg.GetConfig().Active)704})705706t.Run("deactivates record", func(t *testing.T) {707_, client, dbConn := setupOIDCService(t, withOIDCFeatureEnabled)708issuer := newFakeIdP(t, true)709710created := dbtest.CreateOIDCClientConfigs(t, dbConn, db.OIDCClientConfig{711OrganizationID: organizationID,712Issuer: issuer,713Active: true,714Verified: db.BoolPointer(true),715})[0]716717_, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{718Id: created.ID.String(),719OrganizationId: created.OrganizationID.String(),720Activate: false,721}))722require.NoError(t, err)723724getResponse, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{725Id: created.ID.String(),726OrganizationId: created.OrganizationID.String(),727}))728require.NoError(t, err)729require.Equal(t, false, getResponse.Msg.Config.Active)730})731732t.Run("record not found", func(t *testing.T) {733_, client, _ := setupOIDCService(t, withOIDCFeatureEnabled)734735_, err := client.SetClientConfigActivation(context.Background(), connect.NewRequest(&v1.SetClientConfigActivationRequest{736Id: uuid.NewString(),737OrganizationId: organizationID.String(),738Activate: false,739}))740require.Error(t, err)741require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))742})743}744745func setupOIDCService(t *testing.T, expClient experiments.Client) (*protocol.MockAPIInterface, v1connect.OIDCServiceClient, *gorm.DB) {746t.Helper()747748dbConn := dbtest.ConnectForTests(t)749750ctrl := gomock.NewController(t)751t.Cleanup(ctrl.Finish)752753serverMock := protocol.NewMockAPIInterface(ctrl)754755svc := NewOIDCService(&FakeServerConnPool{api: serverMock}, expClient, dbConn, dbtest.CipherSet(t))756757keyset := jwstest.GenerateKeySet(t)758rsa256, err := jws.NewRSA256(keyset)759require.NoError(t, err)760761_, handler := v1connect.NewOIDCServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{762Issuer: "unitetest.com",763Cookie: config.CookieConfig{764Name: "cookie_jwt",765},766}, rsa256)))767768router := chi.NewRouter()769router.Use(middleware.Logger)770router.Mount("/", handler)771ts := httptest.NewServer(router)772t.Cleanup(ts.Close)773774client := v1connect.NewOIDCServiceClient(http.DefaultClient, ts.URL, connect.WithInterceptors(775auth.NewClientInterceptor("auth-token"),776))777778// setup our default user779serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).AnyTimes()780serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil).AnyTimes()781// ensure our user is owner of our default org782dbtest.CreateTeamMembership(t, dbConn, db.OrganizationMembership{783UserID: uuid.MustParse(user.ID),784OrganizationID: organizationID,785Role: db.OrganizationMembershipRole_Owner,786})787788return serverMock, client, dbConn789}790791func newFakeIdP(t *testing.T, discoveryEnabled bool) string {792t.Helper()793794router := chi.NewRouter()795ts := httptest.NewServer(router)796t.Cleanup(ts.Close)797url := ts.URL798799router.Use(middleware.Logger)800if discoveryEnabled {801router.Get("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {802w.Header().Add("Content-Type", "application/json;application/foo")803_, err := w.Write([]byte(`{}`))804require.NoError(t, err)805})806}807return url808}809810811