Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
update FE to add external settings page
  • Loading branch information
Emyrk committed Dec 5, 2023
commit add4d6f00b78d4ed8acc9f2f17fc7f74e6ec0372
6 changes: 6 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,28 @@ import (
"github.com/coder/coder/v2/provisionersdk/proto"
)

func ExternalAuths(auths []database.ExternalAuthLink) []codersdk.ExternalAuthLink {
type ExternalAuthMeta struct {
Authenticated bool
ValidateError string
}

func ExternalAuths(auths []database.ExternalAuthLink, meta map[string]ExternalAuthMeta) []codersdk.ExternalAuthLink {
out := make([]codersdk.ExternalAuthLink, 0, len(auths))
for _, auth := range auths {
out = append(out, ExternalAuth(auth))
out = append(out, ExternalAuth(auth, meta[auth.ProviderID]))
}
return out
}

func ExternalAuth(auth database.ExternalAuthLink) codersdk.ExternalAuthLink {
func ExternalAuth(auth database.ExternalAuthLink, meta ExternalAuthMeta) codersdk.ExternalAuthLink {
return codersdk.ExternalAuthLink{
ProviderID: auth.ProviderID,
CreatedAt: auth.CreatedAt,
UpdatedAt: auth.UpdatedAt,
HasRefreshToken: auth.OAuthRefreshToken != "",
Expires: auth.OAuthExpiry,
Authenticated: meta.Authenticated,
ValidateError: meta.ValidateError,
}
}

Expand Down
32 changes: 31 additions & 1 deletion coderd/externalauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,44 @@ func (api *API) listUserExternalAuths(rw http.ResponseWriter, r *http.Request) {
return
}

// This process of authenticating each external link increases the
// response time. However, it is necessary to more correctly debug
// authentication issues.
// We can do this in parallel if we want to speed it up.
configs := make(map[string]*externalauth.Config)
for _, cfg := range api.ExternalAuthConfigs {
configs[cfg.ID] = cfg
}
// Check if the links are authenticated.
linkMeta := make(map[string]db2sdk.ExternalAuthMeta)
for i, link := range links {
if link.OAuthAccessToken != "" {
cfg, ok := configs[link.ProviderID]
if ok {
newLink, valid, err := cfg.RefreshToken(ctx, api.Database, link)
meta := db2sdk.ExternalAuthMeta{
Authenticated: valid,
}
if err != nil {
meta.ValidateError = err.Error()
}
// Update the link if it was potentially refreshed.
if err == nil && valid {
links[i] = newLink
}
break
}
}
}

// Note: It would be really nice if we could cfg.Validate() the links and
// return their authenticated status. To do this, we would also have to
// refresh expired tokens too. For now, I do not want to cause the excess
// traffic on this request, so the user will have to do this with a separate
// call.
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ListUserExternalAuthResponse{
Providers: ExternalAuthConfigs(api.ExternalAuthConfigs),
Links: db2sdk.ExternalAuths(links),
Links: db2sdk.ExternalAuths(links, linkMeta),
})
}

Expand Down
2 changes: 2 additions & 0 deletions codersdk/externalauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type ExternalAuthLink struct {
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
HasRefreshToken bool `json:"has_refresh_token"`
Expires time.Time `json:"expires" format:"date-time"`
Authenticated bool `json:"authenticated"`
ValidateError string `json:"validate_error"`
}

// ExternalAuthLinkProvider are the static details of a provider.
Expand Down
4 changes: 3 additions & 1 deletion docs/api/git.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion docs/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions site/src/api/queries/externalauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ export const listUserExternalAuths = () => {
};
};

const getUserExternalAuthKey = (providerID: string) => [
providerID,
"get",
"external-auth",
];

export const userExternalAuth = (providerID: string) => {
return {
queryKey: getUserExternalAuthKey(providerID),
queryFn: () => API.getExternalAuthProvider(providerID),
};
};

export const validateExternalAuth = (_: QueryClient) => {
return {
mutationFn: API.getExternalAuthProvider,
Expand Down
2 changes: 2 additions & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 16 additions & 9 deletions site/src/pages/CreateWorkspacePage/ExternalAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ExternalAuthProps {
externalAuthPollingState: ExternalAuthPollingState;
startPollingExternalAuth: () => void;
error?: string;
message?: string;
}

export const ExternalAuth: FC<ExternalAuthProps> = (props) => {
Expand All @@ -26,8 +27,14 @@ export const ExternalAuth: FC<ExternalAuthProps> = (props) => {
externalAuthPollingState,
startPollingExternalAuth,
error,
message,
} = props;

const messageContent =
message ??
(authenticated
? `Authenticated with ${displayName}`
: `Login with ${displayName}`);
return (
<Tooltip
title={authenticated && `${displayName} has already been connected.`}
Expand All @@ -40,12 +47,14 @@ export const ExternalAuth: FC<ExternalAuthProps> = (props) => {
variant="contained"
size="large"
startIcon={
<img
src={displayIcon}
alt={`${displayName} Icon`}
width={16}
height={16}
/>
displayIcon && (
<img
src={displayIcon}
alt={`${displayName} Icon`}
width={16}
height={16}
/>
)
}
disabled={authenticated}
css={{ height: 52 }}
Expand All @@ -61,9 +70,7 @@ export const ExternalAuth: FC<ExternalAuthProps> = (props) => {
startPollingExternalAuth();
}}
>
{authenticated
? `Authenticated with ${displayName}`
: `Login with ${displayName}`}
{messageContent}
</LoadingButton>

{externalAuthPollingState === "abandoned" && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,37 @@ import { getErrorMessage } from "api/errors";

const UserExternalAuthSettingsPage: FC = () => {
const queryClient = useQueryClient();
// This is used to tell the child components something was unlinked and things
// need to be refetched
const [unlinked, setUnlinked] = useState(0);

const userExternalAuthsQuery = useQuery(listUserExternalAuths());
const {
data: externalAuths,
error,
isLoading,
refetch,
} = useQuery(listUserExternalAuths());

const [appToUnlink, setAppToUnlink] = useState<string>();
const unlinkAppMutation = useMutation(unlinkExternalAuths(queryClient));
const mutateParams = unlinkExternalAuths(queryClient);
const unlinkAppMutation = useMutation({
...mutateParams,
onSuccess: async () => {
await mutateParams.onSuccess();
await refetch();
setUnlinked(unlinked + 1);
},
});

const validateAppMutation = useMutation(validateExternalAuth(queryClient));

return (
<Section title="External Authentication">
<UserExternalAuthSettingsPageView
isLoading={userExternalAuthsQuery.isLoading}
getAuthsError={userExternalAuthsQuery.error}
auths={userExternalAuthsQuery.data}
isLoading={isLoading}
getAuthsError={error}
auths={externalAuths}
unlinked={unlinked}
onUnlinkExternalAuth={(providerID: string) => {
setAppToUnlink(providerID);
}}
Expand Down
Loading