Skip to content

Commit

Permalink
feat: Add multiple users to org in a single request
Browse files Browse the repository at this point in the history
Signed-off-by: jay-dee7 <[email protected]>
  • Loading branch information
jay-dee7 committed Dec 21, 2023
1 parent 1a79671 commit 71b188f
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 190 deletions.
16 changes: 16 additions & 0 deletions auth/basic_auth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package auth

import (
"context"
"encoding/base64"
"fmt"
"net/http"
Expand Down Expand Up @@ -72,6 +73,10 @@ func (a *auth) BasicAuth() echo.MiddlewareFunc {
func (a *auth) SkipBasicAuth(ctx echo.Context) bool {
authHeader := ctx.Request().Header.Get(echo.HeaderAuthorization)

if strings.HasPrefix(ctx.Request().URL.Path, "/v2/ext/") {
return true
}

hasJWT := a.checkJWT(authHeader, ctx.Request().Cookies())
if hasJWT {
return true
Expand Down Expand Up @@ -157,6 +162,17 @@ func (a *auth) validateBasicAuthCredentials(auth string) (*types.User, error) {
basicAuthCredentials := strings.Split(string(decodedCredentials), ":")
username, password := basicAuthCredentials[0], basicAuthCredentials[1]

// try login with GitHub PAT
// 1. "github_pat_" prefix is for the new fine-grained, repo scoped tokens
// 2. "ghp_" prefix is for the old (classic) github tokens
if strings.HasPrefix(password, "github_pat_") || strings.HasPrefix(password, "ghp_") {
user, ghErr := a.getUserWithGithubOauthToken(context.Background(), password)
if ghErr != nil {
return nil, fmt.Errorf("ERR_READ_USER_WITH_GITHUB_TOKEN: %w", ghErr)
}

return user, nil
}
user, err := a.validateUser(username, password)
if err != nil {
return nil, err
Expand Down
20 changes: 12 additions & 8 deletions auth/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import (
"time"

"github.com/containerish/OpenRegistry/config"
v2_types "github.com/containerish/OpenRegistry/store/v1/types"
"github.com/containerish/OpenRegistry/types"
"github.com/containerish/OpenRegistry/store/v1/types"
"github.com/google/go-github/v56/github"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -116,6 +115,11 @@ func (a *auth) GithubLoginCallbackHandler(ctx echo.Context) error {
}

if user.GithubConnected {
ghIdentity := user.Identities.GetGitHubIdentity()
ghIdentity.Email = ghUser.GetEmail()
ghIdentity.Username = ghUser.GetLogin()
user.Identities[types.IdentityProviderGitHub] = ghIdentity

_, err = a.userStore.UpdateUser(ctx.Request().Context(), user)
if err != nil {
uri := a.getGitHubErrorURI(ctx, http.StatusConflict, err.Error())
Expand All @@ -136,7 +140,7 @@ func (a *auth) GithubLoginCallbackHandler(ctx echo.Context) error {
return err
}

user.Identities[v2_types.IdentityProviderGitHub] = &v2_types.UserIdentity{
user.Identities[types.IdentityProviderGitHub] = &types.UserIdentity{
ID: fmt.Sprint(ghUser.GetID()),
Name: ghUser.GetName(),
Username: ghUser.GetLogin(),
Expand Down Expand Up @@ -211,14 +215,14 @@ func (a *auth) createCookie(
}

// makes an http request to get user info from token, if it's valid, it's all good :)
func (a *auth) getUserWithGithubOauthToken(ctx context.Context, token string) (*v2_types.User, error) {
func (a *auth) getUserWithGithubOauthToken(ctx context.Context, token string) (*types.User, error) {
req, err := a.ghClient.NewRequest(http.MethodGet, "/user", nil)
if err != nil {
return nil, fmt.Errorf("GH_AUTH_REQUEST_ERROR: %w", err)
}
req.Header.Set(AuthorizationHeaderKey, "token "+token)

var oauthUser v2_types.User
var oauthUser github.User
resp, err := a.ghClient.Do(ctx, req, &oauthUser)
if err != nil {
return nil, fmt.Errorf("GH_AUTH_ERROR: %w", err)
Expand All @@ -228,7 +232,7 @@ func (a *auth) getUserWithGithubOauthToken(ctx context.Context, token string) (*
return nil, fmt.Errorf("GHO_UNAUTHORIZED")
}

user, err := a.userStore.GetUserByEmail(ctx, oauthUser.Email)
user, err := a.userStore.GetUserByEmail(ctx, oauthUser.GetEmail())
if err != nil {
return nil, fmt.Errorf("PG_GET_USER_ERR: %w", err)
}
Expand All @@ -245,7 +249,7 @@ func (a *auth) getGitHubErrorURI(ctx echo.Context, status int, err string) strin
return fmt.Sprintf("%s%s?%s", webAppEndoint, a.c.WebAppConfig.ErrorRedirectPath, queryParams.Encode())
}

func (a *auth) finishGitHubCallback(ctx echo.Context, user *v2_types.User, oauthToken *oauth2.Token) error {
func (a *auth) finishGitHubCallback(ctx echo.Context, user *types.User, oauthToken *oauth2.Token) error {
sessionId, err := uuid.NewRandom()
if err != nil {
return err
Expand Down Expand Up @@ -279,7 +283,7 @@ func (a *auth) finishGitHubCallback(ctx echo.Context, user *v2_types.User, oauth
return nil
}

func (a *auth) storeGitHubUserIfDoesntExist(ctx context.Context, pgErr error, user *v2_types.User) error {
func (a *auth) storeGitHubUserIfDoesntExist(ctx context.Context, pgErr error, user *types.User) error {
if strings.HasSuffix(pgErr.Error(), "no rows in result set") {
id, err := uuid.NewRandom()
if err != nil {
Expand Down
190 changes: 94 additions & 96 deletions auth/oci_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,100 @@ const (
DefaultOCITokenLifetime = time.Minute * 10
)

// Request format: https://openregistry.dev/token?service=registry.docker.io&scope=repository:samalba/my-app:pull,push
func (a *auth) Token(ctx echo.Context) error {
// TODO (jay-dee7) - check for all valid query params here like serive, client_id, offline_token, etc
// more at this link - https://docs.docker.com/registry/spec/auth/token/
ctx.Set(types.HandlerStartTime, time.Now())

scopes, err := ParseOCITokenPermissionRequest(ctx.Request().URL)
if err != nil {
registryErr := common.RegistryErrorResponse(registry.RegistryErrorCodeUnknown, "invalid scope provided", echo.Map{
"error": err.Error(),
})
echoErr := ctx.JSONBlob(http.StatusBadRequest, registryErr.Bytes())
a.logger.Log(ctx, registryErr).Send()
return echoErr
}

// when scopes only have one action, and that action is pull
isPullRequest := len(scopes) == 1 && len(scopes[0].Actions) == 1 && scopes[0].HasPullAccess()
if isPullRequest {
repo, repoErr := a.registryStore.GetRepositoryByNamespace(ctx.Request().Context(), scopes[0].Name)
if repoErr != nil {
registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeNameInvalid,
"requested resource does not exist on the registry",
echo.Map{
"error": repoErr.Error(),
},
)
echoErr := ctx.JSONBlob(http.StatusBadRequest, registryErr.Bytes())
a.logger.Log(ctx, registryErr).Send()
return echoErr
}
user := ctx.Get(string(types.UserContextKey)).(*types.User)

if repo.Visibility == types.RepositoryVisibilityPublic {
token, tokenErr := a.newOCIToken(user.ID, scopes)
if tokenErr != nil {
registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeNameInvalid,
"error creating oci token",
echo.Map{
"error": tokenErr.Error(),
},
)
echoErr := ctx.JSONBlob(http.StatusBadRequest, registryErr.Bytes())
a.logger.Log(ctx, registryErr).Send()
return echoErr
}
now := time.Now()
echoErr := ctx.JSON(http.StatusOK, echo.Map{
"token": token,
"expires_in": now.Add(DefaultOCITokenLifetime).Unix(),
"issued_at": now,
})
a.logger.Log(ctx, nil).Send()
return echoErr
}
}

authHeader := ctx.Request().Header.Get(AuthorizationHeaderKey)
if authHeader != "" && len(scopes) != 0 {
token, authErr := a.tryBasicAuthFlow(ctx, scopes)
if authErr != nil {
registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeUnauthorized,
"authentication failed",
echo.Map{
"error": authErr.Error(),
},
)
echoErr := ctx.JSONBlob(http.StatusUnauthorized, registryErr.Bytes())
a.logger.Log(ctx, authErr).Send()
return echoErr
}
now := time.Now()
echoErr := ctx.JSON(http.StatusOK, echo.Map{
"token": token,
"expires_in": now.Add(DefaultOCITokenLifetime).Unix(),
"issued_at": now,
})
a.logger.Log(ctx, nil).Send()
return echoErr
}

registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeUnauthorized,
"authentication failed",
nil,
)
err = ctx.JSONBlob(http.StatusUnauthorized, registryErr.Bytes())
a.logger.Log(ctx, registryErr).Send()
return err
}

func isOCILoginRequest(url *url.URL) types.OCITokenPermissonClaimList {
account := url.Query().Get(OCITokenQueryParamAccount)
if account != "" {
Expand Down Expand Up @@ -157,102 +251,6 @@ func (a *auth) tryBasicAuthFlow(ctx echo.Context, scopes types.OCITokenPermisson
return "", nil
}

// Token
// request basically comes for
// https://openregistry.dev/token?service=registry.docker.io&scope=repository:samalba/my-app:pull,push
func (a *auth) Token(ctx echo.Context) error {
// TODO (jay-dee7) - check for all valid query params here like serive, client_id, offline_token, etc
// more at this link - https://docs.docker.com/registry/spec/auth/token/
ctx.Set(types.HandlerStartTime, time.Now())

scopes, err := ParseOCITokenPermissionRequest(ctx.Request().URL)
if err != nil {
registryErr := common.RegistryErrorResponse(registry.RegistryErrorCodeUnknown, "invalid scope provided", echo.Map{
"error": err.Error(),
})
echoErr := ctx.JSONBlob(http.StatusBadRequest, registryErr.Bytes())
a.logger.Log(ctx, registryErr).Send()
return echoErr
}

// when scopes only have one action, and that action is pull
isPullRequest := len(scopes) == 1 && len(scopes[0].Actions) == 1 && scopes[0].HasPullAccess()
if isPullRequest {
repo, repoErr := a.registryStore.GetRepositoryByNamespace(ctx.Request().Context(), scopes[0].Name)
if repoErr != nil {
registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeNameInvalid,
"requested resource does not exist on the registry",
echo.Map{
"error": repoErr.Error(),
},
)
echoErr := ctx.JSONBlob(http.StatusBadRequest, registryErr.Bytes())
a.logger.Log(ctx, registryErr).Send()
return echoErr
}
user := ctx.Get(string(types.UserContextKey)).(*types.User)

if repo.Visibility == types.RepositoryVisibilityPublic {
token, tokenErr := a.newOCIToken(user.ID, scopes)
if tokenErr != nil {
registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeNameInvalid,
"error creating oci token",
echo.Map{
"error": tokenErr.Error(),
},
)
echoErr := ctx.JSONBlob(http.StatusBadRequest, registryErr.Bytes())
a.logger.Log(ctx, registryErr).Send()
return echoErr
}
now := time.Now()
echoErr := ctx.JSON(http.StatusOK, echo.Map{
"token": token,
"expires_in": now.Add(DefaultOCITokenLifetime).Unix(),
"issued_at": now,
})
a.logger.Log(ctx, nil).Send()
return echoErr
}
}

authHeader := ctx.Request().Header.Get(AuthorizationHeaderKey)
if authHeader != "" && len(scopes) != 0 {
token, authErr := a.tryBasicAuthFlow(ctx, scopes)
if authErr != nil {
registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeUnauthorized,
"authentication failed",
echo.Map{
"error": authErr.Error(),
},
)
echoErr := ctx.JSONBlob(http.StatusUnauthorized, registryErr.Bytes())
a.logger.Log(ctx, authErr).Send()
return echoErr
}
now := time.Now()
echoErr := ctx.JSON(http.StatusOK, echo.Map{
"token": token,
"expires_in": now.Add(DefaultOCITokenLifetime).Unix(),
"issued_at": now,
})
a.logger.Log(ctx, nil).Send()
return echoErr
}

registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeUnauthorized,
"authentication failed",
nil,
)
err = ctx.JSONBlob(http.StatusUnauthorized, registryErr.Bytes())
a.logger.Log(ctx, registryErr).Send()
return err
}

func (a *auth) getCredsFromHeader(r *http.Request) (string, string, error) {
authHeader := r.Header.Get(AuthorizationHeaderKey)
if authHeader == "" {
Expand Down
Loading

0 comments on commit 71b188f

Please sign in to comment.