diff --git a/Makefile b/Makefile index 58a53202..d559b053 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,12 @@ migup: migdown: migrate -database ${POSTGRESQL_URL} -path db/migrations down +cleanup: migdown migup + +mock-images: + bash ./scripts/mock-images.sh + tools: pip3 install ggshield pre-commit pre-commit install + diff --git a/auth/auth.go b/auth/auth.go index 6870d55e..5b3e2dca 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -20,9 +20,14 @@ type Authentication interface { BasicAuth() echo.MiddlewareFunc Token(ctx echo.Context) error JWT() echo.MiddlewareFunc + JWTRest() echo.MiddlewareFunc ACL() echo.MiddlewareFunc LoginWithGithub(ctx echo.Context) error GithubLoginCallbackHandler(ctx echo.Context) error + ExpireSessions(ctx echo.Context) error + SignOut(ctx echo.Context) error + ReadUserWithSession(ctx echo.Context) error + RenewAccessToken(ctx echo.Context) error } // New is the constructor function returns an Authentication implementation diff --git a/auth/basic_auth.go b/auth/basic_auth.go index f2535f1c..0dc32874 100644 --- a/auth/basic_auth.go +++ b/auth/basic_auth.go @@ -174,26 +174,27 @@ func BasicAuthWithConfig(config middleware.BasicAuthConfig) echo.MiddlewareFunc } // makes an http request to get user info from token, if it's valid, it's all good :) -func (a *auth) validateUserWithGithubOauthToken(ctx context.Context, token string) (bool, 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 false, fmt.Errorf("GH_AUTH_REQUEST_ERROR: %w", err) + return nil, fmt.Errorf("GH_AUTH_REQUEST_ERROR: %w", err) } req.Header.Set(AuthorizationHeaderKey, "token "+token) var oauthUser types.User resp, err := a.ghClient.Do(ctx, req, &oauthUser) if err != nil { - return false, fmt.Errorf("GH_AUTH_ERROR: %w", err) + return nil, fmt.Errorf("GH_AUTH_ERROR: %w", err) } if resp.StatusCode != http.StatusOK { - return false, fmt.Errorf("UNAUTHORIZED") + return nil, fmt.Errorf("GHO_UNAUTHORIZED") } - if _, err = a.pgStore.GetUser(ctx, oauthUser.Email); err != nil { - return false, fmt.Errorf("PG_GET_USER_ERR: %w", err) + user, err := a.pgStore.GetUser(ctx, oauthUser.Email, false) + if err != nil { + return nil, fmt.Errorf("PG_GET_USER_ERR: %w", err) } - return true, nil + return user, nil } diff --git a/auth/github.go b/auth/github.go index ea4084e4..1749ffc1 100644 --- a/auth/github.go +++ b/auth/github.go @@ -2,11 +2,13 @@ package auth import ( "context" + "fmt" + "net" "net/http" - "strings" "time" "github.com/containerish/OpenRegistry/config" + "github.com/containerish/OpenRegistry/types" "github.com/google/uuid" "github.com/labstack/echo/v4" @@ -78,40 +80,8 @@ func (a *auth) GithubLoginCallbackHandler(ctx echo.Context) error { }) } - secure := true - sameSite := http.SameSiteStrictMode - domain := strings.TrimPrefix(a.c.WebAppEndpoint, "https://") - if a.c.Environment == config.Local { - secure = false - sameSite = http.SameSiteLaxMode - domain = "localhost" - } - - accessCookie := &http.Cookie{ - Name: "access", - Value: accessToken, - Path: "/", - Domain: domain, - Expires: time.Now().Add(time.Hour), - MaxAge: AccessCookieMaxAge, - Secure: secure, - SameSite: sameSite, - HttpOnly: true, - } - - refreshCookie := &http.Cookie{ - Name: "refresh", - Value: refreshToken, - Path: "/", - Domain: domain, - Expires: time.Now().Add(time.Hour * 750), - MaxAge: RefreshCookieMaxAge, - Secure: secure, - SameSite: sameSite, - HttpOnly: true, - } - - if err := a.pgStore.AddOAuthUser(ctx.Request().Context(), &oauthUser); err != nil { + oauthUser.Password = refreshToken + if err = a.pgStore.AddOAuthUser(ctx.Request().Context(), &oauthUser); err != nil { ctx.Set(types.HttpEndpointErrorKey, err.Error()) return ctx.JSON(http.StatusInternalServerError, echo.Map{ "error": err.Error(), @@ -119,8 +89,22 @@ func (a *auth) GithubLoginCallbackHandler(ctx echo.Context) error { }) } + sessionId := uuid.NewString() + if err = a.pgStore.AddSession(ctx.Request().Context(), sessionId, refreshToken, oauthUser.Username); err != nil { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "ERR_CREATING_SESSION", + }) + } + val := fmt.Sprintf("%s:%s", sessionId, oauthUser.Id) + + sessionCookie := a.createCookie("session_id", val, false, time.Now().Add(time.Hour*750)) + accessCookie := a.createCookie("access", accessToken, true, time.Now().Add(time.Hour)) + refreshCookie := a.createCookie("refresh", refreshToken, true, time.Now().Add(time.Hour*750)) + ctx.SetCookie(accessCookie) ctx.SetCookie(refreshCookie) + ctx.SetCookie(sessionCookie) a.logger.Log(ctx, nil) return ctx.Redirect(http.StatusTemporaryRedirect, a.c.WebAppRedirectURL) } @@ -129,3 +113,32 @@ const ( AccessCookieMaxAge = int(time.Second * 3600) RefreshCookieMaxAge = int(AccessCookieMaxAge * 3600) ) + +func (a *auth) createCookie(name string, value string, httpOnly bool, expiresAt time.Time) *http.Cookie { + + secure := true + sameSite := http.SameSiteStrictMode + if a.c.Environment == config.Local { + secure = false + sameSite = http.SameSiteLaxMode + } + + webappEndpoint := a.c.WebAppEndpoint + if a.c.Environment == config.Local { + host, _, err := net.SplitHostPort(webappEndpoint) + if err != nil { + webappEndpoint = host + } + } + cookie := &http.Cookie{ + Name: name, + Value: value, + Path: "/", + Domain: webappEndpoint, + Expires: expiresAt, + Secure: secure, + SameSite: sameSite, + HttpOnly: httpOnly, + } + return cookie +} diff --git a/auth/jwt.go b/auth/jwt.go index 01f673f9..efe861a1 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -12,13 +12,14 @@ import ( type Claims struct { jwt.StandardClaims + Type string Access AccessList } type PlatformClaims struct { OauthPayload *oauth2.Token `json:"oauth2_token,omitempty"` jwt.StandardClaims - UserPayload types.User + Type string } type RefreshClaims struct { @@ -31,9 +32,17 @@ type ServiceClaims struct { Access AccessList } +// func (a *auth) newPublicPullToken() (string, error) { - tokenLife := time.Now().Add(time.Hour * 24 * 14).Unix() - claims := a.createClaims("public_pull_user", "", tokenLife) + acl := AccessList{ + { + Type: "repository", + Name: "*/*", + Actions: []string{"pull"}, + }, + } + + claims := a.createClaims("public_pull_user", "", acl) // TODO (jay-dee7)- handle this properly, check for errors and don't set defaults for actions claims.Access[0].Actions = []string{"pull"} @@ -55,7 +64,7 @@ func (a *auth) SignOAuthToken(u types.User, payload *oauth2.Token) (string, stri func (a *auth) newOAuthToken(u types.User, payload *oauth2.Token) (string, string, error) { accessClaims := a.createOAuthClaims(u, payload) - refreshClaims := a.createRefreshClaims(u) + refreshClaims := a.createRefreshClaims(u.Id) accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, &accessClaims) accessSign, err := accessToken.SignedString([]byte(a.c.Registry.SigningSecret)) @@ -75,37 +84,40 @@ func (a *auth) newOAuthToken(u types.User, payload *oauth2.Token) (string, strin //nolint func (a *auth) newServiceToken(u types.User) (string, error) { - u.StripForToken() - claims := a.createServiceClaims(u) + acl := AccessList{ + { + Type: "repository", + Name: fmt.Sprintf("%s/*", u.Username), + Actions: []string{"push", "pull"}, + }, + } + claims := a.createClaims(u.Id, "service", acl) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - sign, err := token.SignedString(a.c.Registry.SigningSecret) + sign, err := token.SignedString([]byte(a.c.Registry.SigningSecret)) if err != nil { - return "", err + return "", fmt.Errorf("error signing secret %w", err) } return sign, nil } -func (a *auth) newWebLoginToken(u types.User) (string, string, error) { - u.StripForToken() - claims := a.createWebLoginClaims(u) - refreshClaims := a.createRefreshClaims(u) - - rawAccess := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - rawRefresh := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) - - accessToken, err := rawAccess.SignedString([]byte(a.c.Registry.SigningSecret)) - if err != nil { - return "", "", err +func (a *auth) newWebLoginToken(userId, username, tokenType string) (string, error) { + acl := AccessList{ + { + Type: "repository", + Name: fmt.Sprintf("%s/*", username), + Actions: []string{"push", "pull"}, + }, } - - refreshToken, err := rawRefresh.SignedString([]byte(a.c.Registry.SigningSecret)) + claims := a.createClaims(userId, tokenType, acl) + raw := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err := raw.SignedString([]byte(a.c.Registry.SigningSecret)) if err != nil { - return "", "", err + return "", err } - return accessToken, refreshToken, nil + return token, nil } //nolint @@ -142,7 +154,6 @@ func (a *auth) createServiceClaims(u types.User) ServiceClaims { // }, func (a *auth) createOAuthClaims(u types.User, token *oauth2.Token) PlatformClaims { claims := PlatformClaims{ - UserPayload: u, OauthPayload: token, StandardClaims: jwt.StandardClaims{ Audience: a.c.Endpoint(), @@ -158,44 +169,35 @@ func (a *auth) createOAuthClaims(u types.User, token *oauth2.Token) PlatformClai return claims } -func (a *auth) createRefreshClaims(u types.User) RefreshClaims { +func (a *auth) createRefreshClaims(userId string) RefreshClaims { claims := RefreshClaims{ - ID: u.Id, + ID: userId, StandardClaims: jwt.StandardClaims{ Audience: a.c.Endpoint(), ExpiresAt: time.Now().Add(time.Hour * 750).Unix(), // Refresh tokens can live longer - Id: uuid.NewString(), + Id: userId, IssuedAt: time.Now().Unix(), Issuer: a.c.Endpoint(), NotBefore: time.Now().Unix(), - Subject: u.Id, + Subject: userId, }, } return claims } -func (a *auth) createWebLoginClaims(u types.User) PlatformClaims { - claims := PlatformClaims{ - UserPayload: u, - StandardClaims: jwt.StandardClaims{ - Audience: a.c.Endpoint(), - ExpiresAt: time.Now().Add(time.Hour).Unix(), - Id: uuid.NewString(), - IssuedAt: time.Now().Unix(), - Issuer: a.c.Endpoint(), - NotBefore: time.Now().Unix(), - Subject: u.Id, - }, - } - - return claims -} - -func (a *auth) newToken(u types.User, tokenLife int64) (string, error) { +func (a *auth) newToken(u *types.User) (string, error) { //for now we're sending same name for sub and name. //TODO when repositories need collaborators - claims := a.createClaims(u.Username, u.Username, tokenLife) + + acl := AccessList{ + { + Type: "repository", + Name: fmt.Sprintf("%s/*", u.Username), + Actions: []string{"push", "pull"}, + }, + } + claims := a.createClaims(u.Id, "access", acl) token := jwt.NewWithClaims(jwt.SigningMethodHS256, &claims) // Generate encoded token and send it as response. @@ -231,24 +233,30 @@ claims format } */ -func (a *auth) createClaims(sub, name string, tokenLife int64) Claims { +func (a *auth) createClaims(id, tokenType string, acl AccessList) Claims { + + var tokenLife int64 + switch tokenType { + case "access": + tokenLife = time.Now().Add(time.Hour).Unix() + case "refresh": + tokenLife = time.Now().Add(time.Hour * 750).Unix() + case "service": + tokenLife = time.Now().Add(time.Hour * 750).Unix() + } + claims := Claims{ StandardClaims: jwt.StandardClaims{ Audience: a.c.Endpoint(), ExpiresAt: tokenLife, - Id: uuid.NewString(), + Id: id, IssuedAt: time.Now().Unix(), Issuer: a.c.Endpoint(), NotBefore: time.Now().Unix(), - Subject: sub, - }, - Access: AccessList{ - { - Type: "repository", - Name: fmt.Sprintf("%s/*", name), - Actions: []string{"push", "pull"}, - }, + Subject: id, }, + Access: acl, + Type: tokenType, } return claims } diff --git a/auth/jwt_middleware.go b/auth/jwt_middleware.go index 421f39f6..1d4fbede 100644 --- a/auth/jwt_middleware.go +++ b/auth/jwt_middleware.go @@ -29,12 +29,15 @@ func (a *auth) JWT() echo.MiddlewareFunc { }, BeforeFunc: middleware.DefaultJWTConfig.BeforeFunc, SuccessHandler: middleware.DefaultJWTConfig.SuccessHandler, - ErrorHandler: middleware.DefaultJWTConfig.ErrorHandler, + ErrorHandler: nil, ErrorHandlerWithContext: func(err error, ctx echo.Context) error { // ErrorHandlerWithContext only logs the failing requtest ctx.Set(types.HandlerStartTime, time.Now()) a.logger.Log(ctx, err) - return ctx.NoContent(http.StatusUnauthorized) + return ctx.JSON(http.StatusUnauthorized, echo.Map{ + "error": err.Error(), + "message": "missing authentication information", + }) }, KeyFunc: middleware.DefaultJWTConfig.KeyFunc, ParseTokenFunc: middleware.DefaultJWTConfig.ParseTokenFunc, @@ -50,9 +53,6 @@ func (a *auth) ACL() echo.MiddlewareFunc { return func(hf echo.HandlerFunc) echo.HandlerFunc { return func(ctx echo.Context) error { ctx.Set(types.HandlerStartTime, time.Now()) - defer func() { - a.logger.Log(ctx, nil) - }() m := ctx.Request().Method if m == http.MethodGet || m == http.MethodHead { @@ -72,12 +72,43 @@ func (a *auth) ACL() echo.MiddlewareFunc { } username := ctx.Param("username") - if claims.Subject == username { + + user, err := a.pgStore.GetUserById(ctx.Request().Context(), claims.Id) + if err != nil { + a.logger.Log(ctx, err) + return ctx.NoContent(http.StatusUnauthorized) + } + if user.Username == username { return hf(ctx) } - a.logger.Log(ctx, fmt.Errorf("ACL: username didn't match from token")) return ctx.NoContent(http.StatusUnauthorized) + } } } + +// JWT basically uses the default JWT middleware by echo, but has a slightly different skipper func +func (a *auth) JWTRest() echo.MiddlewareFunc { + return middleware.JWTWithConfig(middleware.JWTConfig{ + BeforeFunc: middleware.DefaultJWTConfig.BeforeFunc, + SuccessHandler: middleware.DefaultJWTConfig.SuccessHandler, + ErrorHandler: nil, + ErrorHandlerWithContext: func(err error, ctx echo.Context) error { + // ErrorHandlerWithContext only logs the failing requtest + ctx.Set(types.HandlerStartTime, time.Now()) + ctx.Set(types.HttpEndpointErrorKey, err.Error()) + a.logger.Log(ctx, err) + return ctx.JSON(http.StatusUnauthorized, echo.Map{ + "error": err.Error(), + "message": "missing authentication information", + }) + }, + KeyFunc: middleware.DefaultJWTConfig.KeyFunc, + ParseTokenFunc: middleware.DefaultJWTConfig.ParseTokenFunc, + SigningKey: []byte(a.c.Registry.SigningSecret), + SigningKeys: map[string]interface{}{}, + SigningMethod: jwt.SigningMethodHS256.Name, + Claims: &Claims{}, + }) +} diff --git a/auth/renew.go b/auth/renew.go new file mode 100644 index 00000000..0757eb01 --- /dev/null +++ b/auth/renew.go @@ -0,0 +1,68 @@ +package auth + +import ( + "net/http" + "time" + + "github.com/golang-jwt/jwt" + "github.com/labstack/echo/v4" +) + +func (a *auth) RenewAccessToken(ctx echo.Context) error { + c, err := ctx.Cookie("refresh") + if err != nil { + if err == http.ErrNoCookie { + return ctx.JSON(http.StatusUnauthorized, echo.Map{ + "error": err.Error(), + "message": "Unauthorised", + }) + } + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "error getting refresh cookie", + }) + } + refreshCookie := c.Value + var claims Claims + tkn, err := jwt.ParseWithClaims(refreshCookie, &claims, func(token *jwt.Token) (interface{}, error) { + return []byte(a.c.Registry.SigningSecret), nil + }) + if err != nil { + if err == jwt.ErrSignatureInvalid { + return ctx.JSON(http.StatusUnauthorized, echo.Map{ + "error": err.Error(), + "message": "Signature error, unauthorised", + }) + } + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + } + + if !tkn.Valid { + return ctx.JSON(http.StatusUnauthorized, echo.Map{ + "error": "invalid token, unauthorised", + }) + } + + userId := claims.Id + user, err := a.pgStore.GetUserById(ctx.Request().Context(), userId) + if err != nil { + return ctx.JSON(http.StatusUnauthorized, echo.Map{ + "error": err.Error(), + "message": "user not found in database, unauthorised", + }) + } + + tokenString, err := a.newWebLoginToken(userId, user.Username, "access") + if err != nil { + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "message": "error creating new web token", + }) + } + + accessCookie := a.createCookie("access", tokenString, true, time.Now().Add(time.Hour)) + ctx.SetCookie(accessCookie) + return ctx.NoContent(http.StatusNoContent) +} diff --git a/auth/sessions.go b/auth/sessions.go new file mode 100644 index 00000000..54960373 --- /dev/null +++ b/auth/sessions.go @@ -0,0 +1,79 @@ +package auth + +import ( + "net/http" + "strconv" + "strings" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +func (a *auth) ExpireSessions(ctx echo.Context) error { + //queryParamSessionId := ctx.QueryParam("session_id") + cookie, err := ctx.Cookie("session_id") + if err != nil { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "msg": "error while getting cookie", + }) + } + parts := strings.Split(cookie.Value, ":") + if len(parts) != 2 { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": "invalid cookie", + }) + } + + sessionID := parts[0] + userId := parts[1] + + var deleteAllSessions bool + queryParamDeleteAll := ctx.QueryParam("delete_all") + if queryParamDeleteAll != "" { + deleteAllSessions, err = strconv.ParseBool(queryParamDeleteAll) + if err != nil { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "delete_all must be a boolean", + }) + } + _, err := uuid.Parse(userId) + if err != nil { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "invalid user id", + }) + } + + if deleteAllSessions { + err := a.pgStore.DeleteAllSessions(ctx.Request().Context(), userId) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "message": "could not delete all sessions", + }) + } + } + + } + + if sessionID != "" { + _, err := uuid.Parse(sessionID) + if err != nil { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "invalid session id", + }) + } + err = a.pgStore.DeleteSession(ctx.Request().Context(), sessionID, userId) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "message": "could not delete session", + }) + } + } + + return nil +} diff --git a/auth/signin.go b/auth/signin.go index f2e5d49e..0bddedc4 100644 --- a/auth/signin.go +++ b/auth/signin.go @@ -7,6 +7,7 @@ import ( "time" "github.com/containerish/OpenRegistry/types" + "github.com/google/uuid" "github.com/labstack/echo/v4" ) @@ -15,6 +16,7 @@ func (a *auth) SignIn(ctx echo.Context) error { var user types.User if err := json.NewDecoder(ctx.Request().Body).Decode(&user); err != nil { + ctx.Set(types.HttpEndpointErrorKey, err.Error()) return ctx.JSON(http.StatusBadRequest, echo.Map{ "error": err.Error(), }) @@ -33,8 +35,7 @@ func (a *auth) SignIn(ctx echo.Context) error { key = user.Username } - //bz, err := a.store.Get([]byte(key)) - userFromDb, err := a.pgStore.GetUser(ctx.Request().Context(), key) + userFromDb, err := a.pgStore.GetUser(ctx.Request().Context(), key, true) if err != nil { a.logger.Log(ctx, err) return ctx.JSON(http.StatusBadRequest, echo.Map{ @@ -48,35 +49,41 @@ func (a *auth) SignIn(ctx echo.Context) error { return ctx.JSON(http.StatusUnauthorized, errMsg) } - access, refresh, err := a.newWebLoginToken(*userFromDb) + access, err := a.newWebLoginToken(userFromDb.Id, userFromDb.Username, "access") if err != nil { a.logger.Log(ctx, err) return ctx.JSON(http.StatusInternalServerError, echo.Map{ "error": err.Error(), }) } - - accessCookie := &http.Cookie{ - Name: "access", - Value: access, - Expires: time.Now().Add(time.Hour), - Path: "/", + refresh, err := a.newWebLoginToken(userFromDb.Id, userFromDb.Username, "refresh") + if err != nil { + a.logger.Log(ctx, err) + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + }) } - refreshCookie := &http.Cookie{ - Name: "refresh", - Value: refresh, - Expires: time.Now().Add(time.Hour * 750), - Path: "/", + id := uuid.NewString() + if err = a.pgStore.AddSession(ctx.Request().Context(), id, refresh, userFromDb.Username); err != nil { + ctx.Set(types.HttpEndpointErrorKey, err.Error()) + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "ERR_CREATING_SESSION", + }) } - http.SetCookie(ctx.Response(), accessCookie) - http.SetCookie(ctx.Response(), refreshCookie) + sessionId := fmt.Sprintf("%s:%s", id, userFromDb.Id) + sessionCookie := a.createCookie("session_id", sessionId, false, time.Now().Add(time.Hour*750)) + accessCookie := a.createCookie("access", access, true, time.Now().Add(time.Hour)) + refreshCookie := a.createCookie("refresh", refresh, true, time.Now().Add(time.Hour*750)) + a.logger.Log(ctx, err) + ctx.SetCookie(accessCookie) + ctx.SetCookie(refreshCookie) + ctx.SetCookie(sessionCookie) return ctx.JSON(http.StatusOK, echo.Map{ - "token": access, - "refresh": refresh, - "expires_in": time.Now().Add(time.Hour).Unix(), - "issued_at": time.Now().Unix(), + "token": access, + "refresh": refresh, }) } diff --git a/auth/signout.go b/auth/signout.go new file mode 100644 index 00000000..167562d3 --- /dev/null +++ b/auth/signout.go @@ -0,0 +1,44 @@ +package auth + +import ( + "net/http" + "strings" + "time" + + "github.com/containerish/OpenRegistry/types" + "github.com/labstack/echo/v4" +) + +func (a *auth) SignOut(ctx echo.Context) error { + sessionCookie, err := ctx.Cookie("session_id") + if err != nil { + ctx.Set(types.HttpEndpointErrorKey, err.Error()) + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "message": "ERROR_GETTING_SESSION_ID_FOR_SIGN_OUT", + }) + } + parts := strings.Split(sessionCookie.Value, ":") + if len(parts) != 2 { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": "INVALID_SESSION_ID", + }) + } + + sessionId := parts[0] + userId := parts[1] + + if err := a.pgStore.DeleteSession(ctx.Request().Context(), sessionId, userId); err != nil { + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "message": "could not delete sessions", + }) + } + + ctx.SetCookie(a.createCookie("access", "", true, time.Now().Add(-time.Hour))) + ctx.SetCookie(a.createCookie("refresh", "", true, time.Now().Add(-time.Hour))) + ctx.SetCookie(a.createCookie("session_id", "", true, time.Now().Add(-time.Hour))) + return ctx.JSON(http.StatusAccepted, echo.Map{ + "message": "session deleted successfully", + }) +} diff --git a/auth/signup.go b/auth/signup.go index 7b728f7b..6354009e 100644 --- a/auth/signup.go +++ b/auth/signup.go @@ -10,6 +10,7 @@ import ( "unicode" "github.com/containerish/OpenRegistry/types" + "github.com/google/uuid" "github.com/labstack/echo/v4" ) @@ -52,6 +53,7 @@ func (a *auth) SignUp(ctx echo.Context) error { Email: u.Email, Username: u.Username, Password: u.Password, + Id: uuid.NewString(), } err = a.pgStore.AddUser(ctx.Request().Context(), newUser) @@ -62,30 +64,39 @@ func (a *auth) SignUp(ctx echo.Context) error { }) } - accessToken, refreshToken, err := a.newWebLoginToken(*newUser) + accessToken, err := a.newWebLoginToken(newUser.Id, newUser.Username, "access") if err != nil { - return ctx.JSON(http.StatusBadRequest, echo.Map{ + return ctx.JSON(http.StatusInternalServerError, echo.Map{ "error": err.Error(), - "code": "CREATE_NEW_TOKEN", + "code": "CREATE_NEW_ACCESS_TOKEN", }) } - - accessCookie := &http.Cookie{ - Name: "access", - Value: accessToken, - Expires: time.Now().Add(time.Hour), - Path: "/", + refreshToken, err := a.newWebLoginToken(newUser.Id, newUser.Username, "refresh") + if err != nil { + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "code": "CREATE_NEW_REFRESH_TOKEN", + }) } - refreshCookie := &http.Cookie{ - Name: "refresh", - Value: refreshToken, - Expires: time.Now().Add(time.Hour * 750), - Path: "/", + id := uuid.NewString() + if err = a.pgStore.AddSession(ctx.Request().Context(), id, refreshToken, newUser.Username); err != nil { + ctx.Set(types.HttpEndpointErrorKey, err.Error()) + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "ERR_CREATING_SESSION", + }) } - http.SetCookie(ctx.Response(), accessCookie) - http.SetCookie(ctx.Response(), refreshCookie) + sessionId := fmt.Sprintf("%s:%s", id, newUser.Id) + sessionCookie := a.createCookie("session_id", sessionId, false, time.Now().Add(time.Hour*750)) + accessCookie := a.createCookie("access", accessToken, true, time.Now().Add(time.Hour)) + refreshCookie := a.createCookie("refresh", refreshToken, true, time.Now().Add(time.Hour*750)) + + ctx.SetCookie(accessCookie) + ctx.SetCookie(refreshCookie) + ctx.SetCookie(sessionCookie) + a.logger.Log(ctx, err) return ctx.JSON(http.StatusCreated, echo.Map{ "message": "user successfully created", diff --git a/auth/token.go b/auth/token.go index 57e747a4..0d217817 100644 --- a/auth/token.go +++ b/auth/token.go @@ -27,16 +27,28 @@ func (a *auth) Token(ctx echo.Context) error { return ctx.NoContent(http.StatusUnauthorized) } - if strings.HasPrefix(password, "gho_") { - if _, err = a.validateUserWithGithubOauthToken(ctx.Request().Context(), password); err != nil { + if strings.HasPrefix(password, "gho_") || strings.HasPrefix(password, "ghp_") { + user, err := a.getUserWithGithubOauthToken(ctx.Request().Context(), password) + if err != nil { + a.logger.Log(ctx, err) return ctx.JSON(http.StatusUnauthorized, echo.Map{ "error": err.Error(), }) } - token, _ := a.newPublicPullToken() + token, err := a.newServiceToken(*user) + if err != nil { + a.logger.Log(ctx, err) + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "message": "failed to get new service token", + }) + } + a.logger.Log(ctx, nil) return ctx.JSON(http.StatusOK, echo.Map{ - "token": token, + "token": token, + "expires_in": time.Now().Add(time.Hour).Unix(), // look at auth/jwt.go:251 + "issued_at": time.Now(), }) } diff --git a/auth/user.go b/auth/user.go new file mode 100644 index 00000000..b8cf9222 --- /dev/null +++ b/auth/user.go @@ -0,0 +1,48 @@ +package auth + +import ( + "net/http" + "strings" + "time" + + "github.com/containerish/OpenRegistry/types" + "github.com/labstack/echo/v4" +) + +func (a *auth) ReadUserWithSession(ctx echo.Context) error { + ctx.Set(types.HandlerStartTime, time.Now()) + + session, err := ctx.Cookie("session_id") + if err != nil { + ctx.Set(types.HttpEndpointErrorKey, err.Error()) + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "message": "ERROR_GETTING_SESSION_ID", + }) + } + if session.Value == "" { + ctx.Set(types.HttpEndpointErrorKey, "error in getting cookies") + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "msg": "error is cookie", + }) + } + + parts := strings.Split(session.Value, ":") + if len(parts) != 2 { + ctx.Set(types.HttpEndpointErrorKey, "invalid session id") + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": "invalid session id", + }) + } + + sessionId := parts[0] + user, err := a.pgStore.GetUserWithSession(ctx.Request().Context(), sessionId) + if err != nil { + ctx.Set(types.HttpEndpointErrorKey, err.Error()) + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "ERROR_FETCHING_USER_WITH_SESSION", + }) + } + return ctx.JSON(http.StatusOK, user) +} diff --git a/auth/validate_user.go b/auth/validate_user.go index 86610ef1..7f9267e4 100644 --- a/auth/validate_user.go +++ b/auth/validate_user.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/containerish/OpenRegistry/types" "github.com/labstack/echo/v4" ) @@ -14,7 +13,7 @@ func (a *auth) validateUser(username, password string) (map[string]interface{}, return nil, fmt.Errorf("Email/Password cannot be empty") } - userFromDb, err := a.pgStore.GetUser(context.Background(), username) + userFromDb, err := a.pgStore.GetUser(context.Background(), username, true) if err != nil { return nil, err } @@ -23,15 +22,14 @@ func (a *auth) validateUser(username, password string) (map[string]interface{}, return nil, fmt.Errorf("invalid password") } - tokenLife := time.Now().Add(time.Hour * 24 * 14).Unix() - token, err := a.newToken(types.User{Username: username}, tokenLife) + token, err := a.newToken(userFromDb) if err != nil { return nil, err } return echo.Map{ "token": token, - "expires_in": tokenLife, + "expires_in": time.Now().Add(time.Hour).Unix(), // look at auth/jwt.go:251 "issued_at": time.Now(), }, nil } diff --git a/db/migrations/000002_create_blob_table.up.sql b/db/migrations/000002_create_blob_table.up.sql index 72e690a5..5bb763a8 100644 --- a/db/migrations/000002_create_blob_table.up.sql +++ b/db/migrations/000002_create_blob_table.up.sql @@ -1,8 +1,8 @@ CREATE TABLE "blob" ( - "uuid" uuid, - "digest" text PRIMARY KEY, - "sky_link" text, - "start_range" int, - "end_range" int + "uuid" uuid, + "digest" text PRIMARY KEY, + "sky_link" text, + "start_range" int, + "end_range" int, + "created_at" timestamp ); - diff --git a/db/migrations/000003_create_layer_table.up.sql b/db/migrations/000003_create_layer_table.up.sql index 8a3bd4f6..b81cee6f 100644 --- a/db/migrations/000003_create_layer_table.up.sql +++ b/db/migrations/000003_create_layer_table.up.sql @@ -1,8 +1,10 @@ CREATE TABLE "layer" ( - "uuid" uuid PRIMARY KEY, - "digest" text UNIQUE NOT NULL, - "blob_ids" text[], - "media_type" text, - "sky_link" text, - "size" int + "uuid" uuid PRIMARY KEY, + "digest" text UNIQUE NOT NULL, + "blob_ids" text[], + "media_type" text, + "sky_link" text, + "size" int, + "created_at" timestamp, + "updated_at" timestamp ); diff --git a/db/migrations/000004_create_config_table.up.sql b/db/migrations/000004_create_config_table.up.sql index 34c957f7..982538b6 100644 --- a/db/migrations/000004_create_config_table.up.sql +++ b/db/migrations/000004_create_config_table.up.sql @@ -1,13 +1,15 @@ CREATE TABLE "config" ( - "uuid" uuid UNIQUE NOT NULL, - "namespace" text NOT NULL, - "reference" text NOT NULL, - "digest" text NOT NULL, - "sky_link" text, - "media_type" text, - "layers" text[], - "size" int, - PRIMARY KEY(namespace, reference) + "uuid" uuid UNIQUE NOT NULL, + "namespace" text NOT NULL, + "reference" text NOT NULL, + "digest" text NOT NULL, + "sky_link" text, + "media_type" text, + "layers" text[], + "size" int, + "created_at" timestamp, + "updated_at" timestamp, + PRIMARY KEY(namespace, reference) ); CREATE INDEX on config (substr(namespace,1,20) text_pattern_ops); diff --git a/db/migrations/000005_create_image_manifest_table.up.sql b/db/migrations/000005_create_image_manifest_table.up.sql index 02f75671..e2e70615 100644 --- a/db/migrations/000005_create_image_manifest_table.up.sql +++ b/db/migrations/000005_create_image_manifest_table.up.sql @@ -1,7 +1,8 @@ CREATE TABLE "image_manifest" ( - "uuid" uuid PRIMARY KEY, - "namespace" text UNIQUE NOT NULL, - "media_type" text, - "schema_version" int + "uuid" uuid PRIMARY KEY, + "namespace" text UNIQUE NOT NULL, + "media_type" text, + "schema_version" int, + "created_at" timestamp, + "updated_at" timestamp ); - diff --git a/db/migrations/000006_create_session_table.down.sql b/db/migrations/000006_create_session_table.down.sql new file mode 100644 index 00000000..9717b89f --- /dev/null +++ b/db/migrations/000006_create_session_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS session; diff --git a/db/migrations/000006_create_session_table.up.sql b/db/migrations/000006_create_session_table.up.sql new file mode 100644 index 00000000..463bf89d --- /dev/null +++ b/db/migrations/000006_create_session_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE "session" ( + "id" uuid PRIMARY KEY, + "is_active" boolean, + "expires_at" timestamp, + "refresh_token" text UNIQUE NOT NULL, + "owner" uuid references users(id) +); diff --git a/db/migrations/000006_create_sessions_table.down.sql b/db/migrations/000006_create_sessions_table.down.sql deleted file mode 100644 index 63d205dc..00000000 --- a/db/migrations/000006_create_sessions_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS sessions; diff --git a/db/migrations/000006_create_sessions_table.up.sql b/db/migrations/000006_create_sessions_table.up.sql deleted file mode 100644 index 61f3ec6f..00000000 --- a/db/migrations/000006_create_sessions_table.up.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE "sessions" ( - "id" uuid PRIMARY KEY, - "is_active" boolean, - "created_at" timestamp, - "updated_at" timestamp, - "expired_at" timestamp, - "expires_at" timestamp, - "refresh_token" text -); diff --git a/main.go b/main.go index a8979c0d..5dfa7767 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "github.com/containerish/OpenRegistry/cache" "github.com/containerish/OpenRegistry/config" "github.com/containerish/OpenRegistry/registry/v2" + "github.com/containerish/OpenRegistry/registry/v2/extensions" "github.com/containerish/OpenRegistry/router" "github.com/containerish/OpenRegistry/skynet" "github.com/containerish/OpenRegistry/store/postgres" @@ -52,8 +53,13 @@ func main() { e.Logger.Errorf("error creating new container registry: %s", err) return } + ext, err := extensions.New(pgStore, logger) + if err != nil { + e.Logger.Errorf("error creating new container registry extensions api: %s", err) + return + } color.Green("Service Endpoint: %s\n", cfg.Endpoint()) - router.Register(cfg, e, reg, authSvc, localCache, pgStore) + router.Register(cfg, e, reg, authSvc, localCache, pgStore, ext) color.Red("error initialising OpenRegistry Server: %s", e.Start(cfg.Registry.Address())) } diff --git a/registry/v2/extensions/catalog_detail.go b/registry/v2/extensions/catalog_detail.go new file mode 100644 index 00000000..62f0a0ea --- /dev/null +++ b/registry/v2/extensions/catalog_detail.go @@ -0,0 +1,126 @@ +package extensions + +import ( + "net/http" + "strconv" + "time" + + "github.com/containerish/OpenRegistry/store/postgres" + "github.com/containerish/OpenRegistry/telemetry" + "github.com/containerish/OpenRegistry/types" + "github.com/labstack/echo/v4" +) + +type Extenion interface { + CatalogDetail(ctx echo.Context) error + RepositoryDetail(ctx echo.Context) error +} + +type extension struct { + store postgres.PersistentStore + logger telemetry.Logger +} + +func New(store postgres.PersistentStore, logger telemetry.Logger) (Extenion, error) { + return &extension{ + store: store, + logger: logger, + }, nil +} + +// CatalogDetail returns a list of container images, goal is to keep it as light as possible +func (ext *extension) CatalogDetail(ctx echo.Context) error { + ctx.Set(types.HandlerStartTime, time.Now()) + + queryParamPageSize := ctx.QueryParam("n") + queryParamOffset := ctx.QueryParam("last") + namespace := ctx.QueryParam("ns") + var pageSize int64 + var offset int64 + if queryParamPageSize != "" { + ps, err := strconv.ParseInt(ctx.QueryParam("n"), 10, 64) + if err != nil { + ext.logger.Log(ctx, err) + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + } + pageSize = ps + } + + if queryParamOffset != "" { + o, err := strconv.ParseInt(ctx.QueryParam("last"), 10, 64) + if err != nil { + ext.logger.Log(ctx, err) + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + } + offset = o + } + + total, err := ext.store.GetCatalogCount(ctx.Request().Context()) + if err != nil { + ext.logger.Log(ctx, err) + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + }) + } + + catalogWithDetail, err := ext.store.GetCatalogDetail(ctx.Request().Context(), namespace, pageSize, offset) + if err != nil { + ext.logger.Log(ctx, err) + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + }) + } + + ext.logger.Log(ctx, nil) + return ctx.JSON(http.StatusOK, echo.Map{ + "repositories": catalogWithDetail, + "total": total, + }) +} + +// RepositoryDetail returns detail of a particular container image +func (ext *extension) RepositoryDetail(ctx echo.Context) error { + ctx.Set(types.HandlerStartTime, time.Now()) + + queryParamPageSize := ctx.QueryParam("n") + queryParamOffset := ctx.QueryParam("last") + namespace := ctx.QueryParam("ns") + var pageSize int64 + var offset int64 + if queryParamPageSize != "" { + ps, err := strconv.ParseInt(ctx.QueryParam("n"), 10, 64) + if err != nil { + ext.logger.Log(ctx, err) + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + } + pageSize = ps + } + + if queryParamOffset != "" { + o, err := strconv.ParseInt(ctx.QueryParam("last"), 10, 64) + if err != nil { + ext.logger.Log(ctx, err) + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + } + offset = o + } + + repository, err := ext.store.GetRepoDetail(ctx.Request().Context(), namespace, pageSize, offset) + if err != nil { + ext.logger.Log(ctx, err) + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + }) + } + + ext.logger.Log(ctx, nil) + return ctx.JSON(http.StatusOK, repository) +} diff --git a/registry/v2/registry.go b/registry/v2/registry.go index 46bd0de0..77ef6269 100644 --- a/registry/v2/registry.go +++ b/registry/v2/registry.go @@ -419,7 +419,9 @@ func (r *registry) StartUpload(ctx echo.Context) error { SkynetLink: skylink, UUID: uuid.NewString(), BlobDigests: nil, - Size: len(buf.Bytes()), + Size: buf.Len(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } txnOp, err := r.store.NewTxn(ctx.Request().Context()) @@ -545,19 +547,31 @@ func (r *registry) CompleteUpload(ctx echo.Context) error { blobNamespace := fmt.Sprintf("%s/blobs", namespace) skylink, err := r.skynet.Upload(blobNamespace, dig, ubuf.Bytes(), true) if err != nil { - errMsg := r.errorResponse(RegistryErrorCodeBlobUploadInvalid, err.Error(), nil) + errMsg := r.errorResponse(RegistryErrorCodeBlobUploadInvalid, err.Error(), echo.Map{ + "reason": "ERR_SKYNET_UPLOAD", + "error": err.Error(), + }) + r.logger.Log(ctx, fmt.Errorf("%s", errMsg)) return ctx.JSONBlob(http.StatusRequestedRangeNotSatisfiable, errMsg) } txnOp, ok := r.txnMap[id] + if !ok { + errMsg := r.errorResponse(RegistryErrorCodeUnknown, "transaction does not exist for uuid -"+id, nil) + ctx.Set(types.HttpEndpointErrorKey, errMsg) + return ctx.JSONBlob(http.StatusBadRequest, errMsg) + } + layer := &types.LayerV2{ MediaType: "", Digest: dig, SkynetLink: skylink, UUID: id, BlobDigests: txnOp.blobDigests, - Size: len(buf.Bytes()), + Size: ubuf.Len(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if !ok { errMsg := r.errorResponse(RegistryErrorCodeUnknown, "transaction does not exist for uuid -"+id, nil) @@ -650,6 +664,8 @@ func (r *registry) PushManifest(ctx echo.Context) error { MediaType: contentType, Layers: layerIDs, Size: 0, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } val := &types.ImageManifestV2{ @@ -657,6 +673,8 @@ func (r *registry) PushManifest(ctx echo.Context) error { Namespace: namespace, MediaType: "", SchemaVersion: 2, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } txnOp, err := r.store.NewTxn(context.Background()) @@ -669,14 +687,14 @@ func (r *registry) PushManifest(ctx echo.Context) error { return ctx.JSONBlob(http.StatusInternalServerError, errMsg) } - if err := r.store.SetManifest(ctx.Request().Context(), txnOp, val); err != nil { + if err = r.store.SetManifest(ctx.Request().Context(), txnOp, val); err != nil { errMsg := r.errorResponse(RegistryErrorCodeUnknown, err.Error(), nil) r.logger.Log(ctx, fmt.Errorf("%s", errMsg)) _ = r.store.Abort(ctx.Request().Context(), txnOp) return ctx.JSONBlob(http.StatusBadRequest, errMsg) } - if err := r.store.SetConfig(ctx.Request().Context(), txnOp, mfc); err != nil { + if err = r.store.SetConfig(ctx.Request().Context(), txnOp, mfc); err != nil { errMsg := r.errorResponse(RegistryErrorCodeUnknown, err.Error(), nil) r.logger.Log(ctx, fmt.Errorf("%s", errMsg)) _ = r.store.Abort(ctx.Request().Context(), txnOp) @@ -814,7 +832,6 @@ func (r *registry) DeleteLayer(ctx echo.Context) error { // Should also look into 401 Code // https://docs.docker.com/registry/spec/api/ func (r *registry) ApiVersion(ctx echo.Context) error { - ctx.Response().Header().Set(HeaderDockerDistributionApiVersion, "registry/2.0") return ctx.String(http.StatusOK, "OK\n") } @@ -834,5 +851,17 @@ func (r *registry) GetImageNamespace(ctx echo.Context) error { "message": "error getting image namespace", }) } - return ctx.JSON(http.StatusOK, result) + + total, err := r.store.GetCatalogCount(ctx.Request().Context()) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + "message": "ERR_GET_CATALOG_COUNT", + }) + } + + return ctx.JSON(http.StatusOK, echo.Map{ + "repositories": result, + "total": total, + }) } diff --git a/registry/v2/types.go b/registry/v2/types.go index d106e9ac..e4581c90 100644 --- a/registry/v2/types.go +++ b/registry/v2/types.go @@ -88,15 +88,15 @@ const ( type ( registry struct { - log zerolog.Logger b blobs logger telemetry.Logger localCache cache.Store + store postgres.PersistentStore skynet *skynet.Client mu *sync.RWMutex - debug bool txnMap map[string]TxnStore - store postgres.PersistentStore + log zerolog.Logger + debug bool } TxnStore struct { @@ -106,45 +106,45 @@ type ( } blobs struct { - mutex sync.Mutex contents map[string][]byte uploads map[string][]byte layers map[string][]string registry *registry + mutex sync.Mutex } ManifestList struct { - SchemaVersion int `json:"schemaVersion"` - MediaType string `json:"mediaType"` - Manifests []struct { + MediaType string `json:"mediaType"` + Manifests []struct { MediaType string `json:"mediaType"` - Size int `json:"size"` Digest string `json:"digest"` Platform struct { Architecture string `json:"architecture"` Os string `json:"os"` Features []string `json:"features"` } `json:"platform"` + Size int `json:"size"` } `json:"manifests"` + SchemaVersion int `json:"schemaVersion"` } ImageManifest struct { - SchemaVersion int `json:"schemaVersion"` + Config Config `json:"config"` MediaType string `json:"mediaType"` Layers Layers `json:"layers"` - Config Config `json:"config"` + SchemaVersion int `json:"schemaVersion"` } Layers []struct { MediaType string `json:"mediaType"` - Size int `json:"size"` Digest string `json:"digest"` + Size int `json:"size"` } Config struct { MediaType string `json:"mediaType"` - Size int `json:"size"` Digest string `json:"digest"` + Size int `json:"size"` } ) diff --git a/router/helpers.go b/router/helpers.go index 3c48cd66..f3edcba2 100644 --- a/router/helpers.go +++ b/router/helpers.go @@ -17,7 +17,10 @@ func RegisterAuthRoutes(authRouter *echo.Group, authSvc auth.Authentication) { authRouter.Add(http.MethodPost, "/signup", authSvc.SignUp) authRouter.Add(http.MethodPost, "/signin", authSvc.SignIn) authRouter.Add(http.MethodPost, "/token", authSvc.SignIn) - + authRouter.Add(http.MethodDelete, "/signout", authSvc.SignOut) + authRouter.Add(http.MethodGet, "/sessions/me", authSvc.ReadUserWithSession) + authRouter.Add(http.MethodDelete, "/sessions", authSvc.ExpireSessions) + authRouter.Add(http.MethodGet, "/renew", authSvc.RenewAccessToken) } // RegisterBetaRoutes contains the experimental features, the betas diff --git a/router/route_names.go b/router/route_names.go index e95994b6..0df1dc52 100644 --- a/router/route_names.go +++ b/router/route_names.go @@ -46,7 +46,18 @@ const ( // Catalog is used to list the available repositories Catalog = "/_catalog" + // Prefix for Extensions + Ext = "/ext" + + // Catalog Extensions API Prefix + C = Ext + "/catalog" + // JWT based auth endpoint TokenAuth = "/token" - Search = "/catalog/search" + Search = C + "/search" + + // API to get detailed catalog information + CatalogDetail = C + "/detail" + + RepositoryDetail = Ext + "/repository" ) diff --git a/router/router.go b/router/router.go index b147074f..ca5a940f 100644 --- a/router/router.go +++ b/router/router.go @@ -7,6 +7,7 @@ import ( "github.com/containerish/OpenRegistry/cache" "github.com/containerish/OpenRegistry/config" "github.com/containerish/OpenRegistry/registry/v2" + "github.com/containerish/OpenRegistry/registry/v2/extensions" "github.com/containerish/OpenRegistry/store/postgres" "github.com/google/uuid" "github.com/labstack/echo-contrib/prometheus" @@ -23,6 +24,7 @@ func Register( authSvc auth.Authentication, localCache cache.Store, pStore postgres.PersistentStore, + ext extensions.Extenion, ) { e.Use(middleware.Recover()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ @@ -67,7 +69,7 @@ func Register( RegisterAuthRoutes(authRouter, authSvc) RegisterBetaRoutes(betaRouter, localCache) InternalRoutes(internal, pStore) - Extensions(v2Router, reg) + Extensions(v2Router, reg, ext) } // RegisterNSRoutes is one of the helper functions to Register @@ -124,10 +126,13 @@ func RegisterNSRoutes(nsRouter *echo.Group, reg registry.Registry) { } // Extensions for teh OCI dist spec -func Extensions(group *echo.Group, reg registry.Registry) { +func Extensions(group *echo.Group, reg registry.Registry, ext extensions.Extenion) { // GET /v2/_catalog group.Add(http.MethodGet, Catalog, reg.Catalog) + // Auto-complete image search group.Add(http.MethodGet, Search, reg.GetImageNamespace) + group.Add(http.MethodGet, CatalogDetail, ext.CatalogDetail) + group.Add(http.MethodGet, RepositoryDetail, ext.RepositoryDetail) } diff --git a/skynet/skynet.go b/skynet/skynet.go index f30a0413..c3357007 100644 --- a/skynet/skynet.go +++ b/skynet/skynet.go @@ -9,6 +9,7 @@ import ( "github.com/SkynetLabs/go-skynet/v2" "github.com/containerish/OpenRegistry/config" + "github.com/fatih/color" tar "github.com/whyrusleeping/tar-utils" ) @@ -20,6 +21,7 @@ func NewClient(oc *config.OpenRegistryConfig) *Client { CustomCookie: oc.SkynetConfig.ApiKey, } + color.Green("Skynet Portal: %s", oc.SkynetConfig.SkynetPortalURL) skynetClient := skynet.NewCustom(oc.SkynetConfig.SkynetPortalURL, opts) httpClient := NewHttpClientForSkynet() diff --git a/store/postgres/container_image.go b/store/postgres/container_image.go index d987e7b0..5822a7f3 100644 --- a/store/postgres/container_image.go +++ b/store/postgres/container_image.go @@ -26,6 +26,8 @@ func (p *pg) GetLayer(ctx context.Context, digest string) (*types.LayerV2, error &layer.MediaType, &layer.SkynetLink, &layer.Size, + &layer.CreatedAt, + &layer.UpdatedAt, ); err != nil { return nil, err } @@ -38,7 +40,19 @@ func (p *pg) SetLayer(ctx context.Context, txn pgx.Tx, l *types.LayerV2) error { childCtx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - _, err := txn.Exec(childCtx, queries.SetLayer, l.MediaType, l.Digest, l.SkynetLink, l.UUID, l.BlobDigests, l.Size) + _, err := txn.Exec( + childCtx, + queries.SetLayer, + l.MediaType, + l.Digest, + l.SkynetLink, + l.UUID, + l.BlobDigests, + l.Size, + l.CreatedAt, + l.UpdatedAt, + ) + return err } @@ -53,6 +67,8 @@ func (p *pg) GetManifest(ctx context.Context, namespace string) (*types.ImageMan &im.Namespace, &im.MediaType, &im.SchemaVersion, + &im.CreatedAt, + &im.UpdatedAt, ); err != nil { return nil, err } @@ -78,6 +94,8 @@ func (p *pg) GetManifestByReference(ctx context.Context, namespace string, ref s &im.MediaType, &im.Layers, &im.Size, + &im.CreatedAt, + &im.UpdatedAt, ); err != nil { return nil, err } @@ -88,7 +106,17 @@ func (p *pg) SetManifest(ctx context.Context, txn pgx.Tx, im *types.ImageManifes childCtx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - _, err := txn.Exec(childCtx, queries.SetImageManifest, im.Uuid, im.Namespace, im.MediaType, im.SchemaVersion) + _, err := txn.Exec( + childCtx, + queries.SetImageManifest, + im.Uuid, + im.Namespace, + im.MediaType, + im.SchemaVersion, + im.CreatedAt, + im.UpdatedAt, + ) + return err } @@ -112,6 +140,7 @@ func (p *pg) GetBlob(ctx context.Context, digest string) ([]*types.Blob, error) &blob.Skylink, &blob.RangeStart, &blob.RangeEnd, + &blob.CreatedAt, ); err != nil { return nil, err } @@ -126,7 +155,8 @@ func (p *pg) SetBlob(ctx context.Context, txn pgx.Tx, b *types.Blob) error { childCtx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - _, err := txn.Exec(childCtx, queries.SetBlob, b.UUID, b.Digest, b.Skylink, b.RangeStart, b.RangeEnd) + _, err := txn.Exec(childCtx, queries.SetBlob, b.UUID, b.Digest, b.Skylink, b.RangeStart, b.RangeEnd, b.CreatedAt) + return err } @@ -153,6 +183,8 @@ func (p *pg) GetConfig(ctx context.Context, namespace string) ([]*types.ConfigV2 &cfg.MediaType, &cfg.Layers, &cfg.Size, + &cfg.CreatedAt, + &cfg.UpdatedAt, ); err != nil { return nil, err } @@ -189,7 +221,8 @@ func (p *pg) SetConfig(ctx context.Context, txn pgx.Tx, cfg types.ConfigV2) erro childCtx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - if _, err := txn.Exec(childCtx, + if _, err := txn.Exec( + childCtx, queries.SetConfig, cfg.UUID, cfg.Namespace, @@ -199,6 +232,8 @@ func (p *pg) SetConfig(ctx context.Context, txn pgx.Tx, cfg types.ConfigV2) erro cfg.MediaType, cfg.Layers, cfg.Size, + cfg.CreatedAt, + cfg.UpdatedAt, ); err != nil { return err } @@ -217,7 +252,7 @@ func (p *pg) GetCatalogCount(ctx context.Context) (int64, error) { return count, nil } -func (p *pg) GetCatalog(ctx context.Context, ns string, pageSize, offset int64) ([]*types.ConfigV2, error) { +func (p *pg) GetCatalog(ctx context.Context, ns string, pageSize, offset int64) ([]string, error) { childCtx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -247,20 +282,116 @@ func (p *pg) GetCatalog(ctx context.Context, ns string, pageSize, offset int64) defer rows.Close() - cfgList := make([]*types.ConfigV2, 0) + var repositories []string for i := 0; rows.Next(); i++ { - var cfg types.ConfigV2 + var repo string + if err := rows.Scan(&repo); err != nil { + return nil, err + } + + repositories = append(repositories, repo) + } + + return repositories, nil +} + +// GetCatalogDetail - ns -> Namespace; ps -> PageSize +func (p *pg) GetCatalogDetail(ctx context.Context, ns string, ps, offset int64) ([]*types.ImageManifestV2, error) { + childCtx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + var rows pgx.Rows + var err error + + if ps != 0 { + rows, err = p.conn.Query(childCtx, queries.GetCatalogDetailWithPagination, ps, offset) + if err != nil { + err = fmt.Errorf("ERR_CATALOG_WITH_PAGINATION: %w", err) + } + } else { + rows, err = p.conn.Query(childCtx, queries.GetCatalogDetailWithPagination, 10, 0) + if err != nil { + err = fmt.Errorf("ERR_CATALOG: %w", err) + } + } + + if ns != "" { + rows, err = p.conn.Query(childCtx, queries.GetUserCatalogDetailWithPagination, ns+"/%", ps, offset) + if err != nil { + err = fmt.Errorf("ERR_USER_CATALOG: %w", err) + } + } + + if err != nil { + return nil, err + } + + defer rows.Close() + var catalog []*types.ImageManifestV2 + + for i := 0; rows.Next(); i++ { + var mf types.ImageManifestV2 + if err := rows.Scan( - &cfg.UUID, - &cfg.Namespace, - &cfg.Reference, - &cfg.Digest, + &mf.Namespace, + &mf.CreatedAt, + &mf.UpdatedAt, ); err != nil { return nil, err } - cfgList = append(cfgList, &cfg) + + catalog = append(catalog, &mf) } - return cfgList, nil + + return catalog, nil +} + +func (p *pg) GetRepoDetail(ctx context.Context, ns string, pageSize, offset int64) (*types.Repository, error) { + childCtx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + var rows pgx.Rows + var err error + + if pageSize != 0 { + rows, err = p.conn.Query(childCtx, queries.GetRepoDetailWithPagination, ns, pageSize, offset) + if err != nil { + err = fmt.Errorf("ERR_REPO_DETAIL_WITH_PAGINATION: %w", err) + } + } else { + rows, err = p.conn.Query(childCtx, queries.GetRepoDetailWithPagination, ns, 10, 0) + if err != nil { + err = fmt.Errorf("ERR_REPO_DETAIL: %w", err) + } + } + + if err != nil { + return nil, err + } + + defer rows.Close() + var repo types.Repository + + for i := 0; rows.Next(); i++ { + var tag types.ConfigV2 + + if err := rows.Scan( + &tag.Reference, + &tag.Digest, + &tag.Skylink, + &tag.Size, + &tag.CreatedAt, + &tag.UpdatedAt, + ); err != nil { + return nil, err + } + + repo.Tags = append(repo.Tags, &tag) + } + + // why get it from db? + repo.Namespace = ns + return &repo, nil } func (p *pg) DeleteLayerV2(ctx context.Context, txn pgx.Tx, digest string) error { @@ -337,7 +468,7 @@ func (p *pg) Metadata(ctx echo.Context) error { return ctx.JSON(http.StatusOK, imageManifestList) } -func (p *pg) GetImageNamespace(ctx context.Context, search string) ([]string, error) { +func (p *pg) GetImageNamespace(ctx context.Context, search string) ([]*types.ImageManifestV2, error) { childCtx, cancel := context.WithTimeout(context.Background(), time.Minute*30) defer cancel() rows, err := p.conn.Query(childCtx, queries.GetImageNamespace, "%"+search+"%") @@ -346,13 +477,18 @@ func (p *pg) GetImageNamespace(ctx context.Context, search string) ([]string, er } defer rows.Close() - var result []string + var result []*types.ImageManifestV2 for rows.Next() { - var ns string - if err := rows.Scan(&ns); err != nil { + var mf types.ImageManifestV2 + if err := rows.Scan( + &mf.Uuid, + &mf.Namespace, + &mf.CreatedAt, + &mf.UpdatedAt, + ); err != nil { return nil, fmt.Errorf("ERR_IMAGE_NAMESPACE_SCAN: %w", err) } - result = append(result, ns) + result = append(result, &mf) } return result, nil } diff --git a/store/postgres/postgres.go b/store/postgres/postgres.go index aa8b20ec..da141403 100644 --- a/store/postgres/postgres.go +++ b/store/postgres/postgres.go @@ -15,6 +15,7 @@ import ( type PersistentStore interface { UserStore RegistryStore + SessionStore Get(key []byte) ([]byte, error) Set(key, value []byte) error GetDigest(digest string) (*types.LayerRef, error) @@ -42,10 +43,16 @@ type AuthStore interface { type UserStore interface { AddUser(ctx context.Context, u *types.User) error AddOAuthUser(ctx context.Context, u *types.User) error - GetUser(ctx context.Context, identifier string) (*types.User, error) + UserExists(ctx context.Context, id string) bool + GetUser(ctx context.Context, identifier string, wihtPassword bool) (*types.User, error) + GetUserById(ctx context.Context, userId string) (*types.User, error) + GetUserWithSession(ctx context.Context, sessionId string) (*types.User, error) UpdateUser(ctx context.Context, identifier string, u *types.User) error DeleteUser(ctx context.Context, identifier string) error IsActive(ctx context.Context, identifier string) bool + AddSession(ctx context.Context, sessionId, refreshToken, owner string) error + DeleteSession(ctx context.Context, sessionId, userId string) error + DeleteAllSessions(ctx context.Context, userId string) error } type RegistryStore interface { @@ -62,14 +69,23 @@ type RegistryStore interface { GetBlob(ctx context.Context, digest string) ([]*types.Blob, error) GetConfig(ctx context.Context, namespace string) ([]*types.ConfigV2, error) GetImageTags(ctx context.Context, namespace string) ([]string, error) - GetCatalog(ctx context.Context, namespace string, pageSize int64, offset int64) ([]*types.ConfigV2, error) + GetCatalog(ctx context.Context, namespace string, pageSize int64, offset int64) ([]string, error) + GetCatalogDetail(ctx context.Context, namespace string, pageSize int64, offset int64) ([]*types.ImageManifestV2, error) + GetRepoDetail(ctx context.Context, namespace string, pageSize int64, offset int64) (*types.Repository, error) GetCatalogCount(ctx context.Context) (int64, error) - GetImageNamespace(ctx context.Context, search string) ([]string, error) + GetImageNamespace(ctx context.Context, search string) ([]*types.ImageManifestV2, error) DeleteLayerV2(ctx context.Context, txn pgx.Tx, digest string) error DeleteBlobV2(ctx context.Context, txn pgx.Tx, digest string) error DeleteManifestOrTag(ctx context.Context, txn pgx.Tx, reference string) error } +type SessionStore interface { + AddSession(ctx context.Context, id, refreshToken, username string) error + GetSession(ctx context.Context, sessionId string) (*types.Session, error) + DeleteSession(ctx context.Context, sessionId, userId string) error + DeleteAllSessions(ctx context.Context, userId string) error +} + type pg struct { conn *pgxpool.Pool } diff --git a/store/postgres/queries/auth.go b/store/postgres/queries/auth.go index fb6d3754..58eadae6 100644 --- a/store/postgres/queries/auth.go +++ b/store/postgres/queries/auth.go @@ -1,11 +1 @@ package queries - -var ( - //nolint - AddSession = `insert into sessions (id, is_active, created_at, expires_at, refresh_token) - values ($1, $2, $3, $4, $5);` - //nolint - UpdateSession = `update sessions set is_active=$2, updated_at=$3, expired_at=$4 where id=$1` - //nolint - ExpireSession = `update sessions set is_active=false, updated_at=NOW(), expired_at=NOW() where id=$1` -) diff --git a/store/postgres/queries/registry.go b/store/postgres/queries/registry.go index 51bca49b..822996f4 100644 --- a/store/postgres/queries/registry.go +++ b/store/postgres/queries/registry.go @@ -3,19 +3,18 @@ package queries // insert queries var ( - // - SetImageManifest = `insert into image_manifest (uuid, namespace, media_type, schema_version) - values ($1, $2, $3, $4) on conflict (namespace) do update set schema_version=3` - SetLayer = `insert into layer (media_type, digest, sky_link, uuid, blob_ids, size) - values ($1, $2, $3, $4, $5, $6) on conflict (digest) do nothing;` + SetImageManifest = `insert into image_manifest (uuid, namespace, media_type, schema_version, created_at, updated_at) + values ($1, $2, $3, $4, $5, $6) on conflict (namespace) do update set updated_at=$6` + SetLayer = `insert into layer (media_type, digest, sky_link, uuid, blob_ids, size,created_at,updated_at) + values ($1, $2, $3, $4, $5, $6,$7,$8) on conflict (digest) do update set updated_at=$8;` // SetBlob TODO - (guacamole/jay-dee7) find a better way to handle duplicates in blob - SetBlob = `insert into blob (uuid, digest, sky_link, start_range, end_range) - values ($1, $2, $3, $4, $5) on conflict (digest) do nothing;` + SetBlob = `insert into blob (uuid, digest, sky_link, start_range, end_range, created_at) + values ($1, $2, $3, $4, $5, $6) on conflict (digest) do nothing;` - SetConfig = `insert into config (uuid, namespace, reference, digest, sky_link, media_type, layers, size) - values ($1, $2, $3, $4, $5, $6,$7, $8) on conflict (namespace,reference) - do update set digest=$4, sky_link=$5,layers=$7;` + SetConfig = `insert into config (uuid, namespace, reference, digest, sky_link, media_type, layers, size, created_at, updated_at) + values ($1, $2, $3, $4, $5, $6,$7, $8, $9, $10) on conflict (namespace,reference) + do update set digest=$4, sky_link=$5,layers=$7,updated_at=$10;` ) // select queries @@ -29,11 +28,17 @@ var ( GetImageTags = `select reference from config where namespace=$1;` GetManifestByRef = `select * from config where namespace=$1 and reference=$2;` GetManifestByDig = `select * from config where namespace=$1 and digest=$2;` - GetCatalogCount = `select count(*) from config;` - GetCatalog = `select uuid,namespace,reference,digest from config;` - GetCatalogWithPagination = `select uuid,namespace,reference,digest from config limit $1 offset $2;` - GetUserCatalogWithPagination = `select uuid,namespace,reference,digest from config where namespace like $1 limit $2 offset $3;` - GetImageNamespace = `select namespace from config where substr(namespace, 1, 50) like $1;` + GetCatalogCount = `select count(*) from image_manifest;` + GetCatalog = `select namespace from image_manifest;` + GetCatalogWithPagination = `select namespace from image_manifest limit $1 offset $2;` + GetUserCatalogWithPagination = `select namespace from image_manifest where namespace like $1 limit $2 offset $3;` + GetImageNamespace = `select uuid,namespace,created_at::timestamptz,updated_at::timestamptz from image_manifest where substr(namespace, 1, 50) like $1;` + + GetCatalogDetailWithPagination = `select namespace,created_at::timestamptz,updated_at::timestamptz from image_manifest limit $1 offset $2;` + GetUserCatalogDetailWithPagination = `select namespace,created_at::timestamptz,updated_at::timestamptz from image_manifest where namespace like $1 limit $2 offset $3;` + + // select floor(sum(size::float4/1000000)) from layer where digest = ANY(ARRAY(select layers from config where namespace='johndoe/traefik')); + GetRepoDetailWithPagination = `select reference, digest, sky_link, (select sum(size) from layer where digest = ANY(layers)) as size, created_at::timestamptz, updated_at::timestamptz from config where namespace=$1 limit $2 offset $3;` ) // delete queries diff --git a/store/postgres/queries/users.go b/store/postgres/queries/users.go index f353ec4d..e3f23304 100644 --- a/store/postgres/queries/users.go +++ b/store/postgres/queries/users.go @@ -2,14 +2,23 @@ package queries var ( - AddUser = `insert into users (id, is_active, username, email, password, created_at, updated_at) -values ($1, $2, $3, $4, $5, $6, $7);` - GetUser = `select id, is_active, username, email, password, created_at, updated_at from users where email=$1 - or username=$1;` - UpdateUser = `update user set username = $1, email = $2, password = $3, updated_at = $4 where username = $5;` - DeleteUser = `delete from user where username = $1;` - GetAllEmails = `select email from users;` - AddOAuthUser = `insert into users (id, username, email, created_at, updated_at, + AddUser = `insert into users (id, is_active, username, name, email, password, created_at, updated_at) +values ($1, $2, $3, $4, $5, $6, $7, $8);` + GetUser = `select id, is_active, username, email, created_at, updated_at from users where email=$1 or username=$1;` + GetUserWithPassword = `select id, is_active, username, email, password, created_at, updated_at from users where email=$1 or username=$1;` + GetUserById = `select id, is_active, username, email, created_at, updated_at from users where id=$1;` + GetUserWithSession = `select id, is_active, name, username, email, created_at, updated_at from users where id=(select owner from session where id=$1);` + UpdateUser = `update user set username = $1, email = $2, password = $3, updated_at = $4 where username = $5;` + DeleteUser = `delete from user where username = $1;` + GetAllEmails = `select email from users;` + AddOAuthUser = `insert into users (id, username, email, created_at, updated_at, bio, type, gravatar_id, login, name, node_id, avatar_url, oauth_id, is_active, hireable) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) on conflict (email) do update set username=$2, email=$3` ) + +var ( + AddSession = `insert into session (id,refresh_token,owner) values($1, $2, (select id from users where username=$3));` + GetSession = `select id,refresh_token,owner from session where id=$1;` + DeleteSession = `delete from session where id=$1 and owner=$2;` + DeleteAllSessions = `delete from session where owner=$1;` +) diff --git a/store/postgres/sessions.go b/store/postgres/sessions.go new file mode 100644 index 00000000..9873de47 --- /dev/null +++ b/store/postgres/sessions.go @@ -0,0 +1,56 @@ +package postgres + +import ( + "context" + "fmt" + "time" + + "github.com/containerish/OpenRegistry/store/postgres/queries" + "github.com/containerish/OpenRegistry/types" + "github.com/jackc/pgx/v4" +) + +func (p *pg) AddSession(ctx context.Context, id, refreshToken, username string) error { + childCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + _, err := p.conn.Exec(childCtx, queries.AddSession, id, refreshToken, username) + if err != nil { + return fmt.Errorf("ERR_CREATE_SESSION: %w", err) + } + return nil +} + +func (p *pg) GetSession(ctx context.Context, sessionId string) (*types.Session, error) { + childCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + row := p.conn.QueryRow(childCtx, queries.GetSession, sessionId) + var session types.Session + if err := row.Scan(&session.Id, &session.RefreshToken, &session.Owner); err != nil || err == pgx.ErrNoRows { + return nil, fmt.Errorf("ERROR_SESSION_LOOKUP: %w", err) + } + return &session, nil +} + +func (p *pg) DeleteSession(ctx context.Context, sessionId, userId string) error { + childCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + _, err := p.conn.Exec(childCtx, queries.DeleteSession, sessionId, userId) + if err != nil { + return fmt.Errorf("ERR_DELETE_SESSION: %w", err) + } + return nil +} + +func (p *pg) DeleteAllSessions(ctx context.Context, userId string) error { + childCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + _, err := p.conn.Exec(childCtx, queries.DeleteAllSessions, userId) + if err != nil { + return fmt.Errorf("ERR_DELETE_ALL_SESSIONS: %w", err) + } + return nil +} diff --git a/store/postgres/users.go b/store/postgres/users.go index 01c26e12..cb353446 100644 --- a/store/postgres/users.go +++ b/store/postgres/users.go @@ -19,8 +19,7 @@ func (p *pg) AddUser(ctx context.Context, u *types.User) error { defer cancel() t := time.Now() - id := uuid.New() - _, err := p.conn.Exec(childCtx, queries.AddUser, id.String(), true, u.Username, u.Email, u.Password, t, t) + _, err := p.conn.Exec(childCtx, queries.AddUser, u.Id, true, u.Username, u.Name, u.Email, u.Password, t, t) if err != nil { return fmt.Errorf("error adding user to database: %w", err) } @@ -65,11 +64,51 @@ func (p *pg) AddOAuthUser(ctx context.Context, u *types.User) error { return nil } -func (p *pg) GetUser(ctx context.Context, identifier string) (*types.User, error) { +func (p *pg) GetUser(ctx context.Context, identifier string, withPassword bool) (*types.User, error) { childCtx, cancel := context.WithTimeout(ctx, time.Millisecond*100) defer cancel() + var user types.User + if withPassword { + row := p.conn.QueryRow(childCtx, queries.GetUserWithPassword, identifier) + + err := row.Scan( + &user.Id, + &user.IsActive, + &user.Username, + &user.Email, + &user.Password, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("ERR_GET_USER_WITH_PASSWORD_FROM_DB: %w", err) + } + + return &user, nil + } + row := p.conn.QueryRow(childCtx, queries.GetUser, identifier) + err := row.Scan( + &user.Id, + &user.IsActive, + &user.Username, + &user.Email, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("ERR_GET_USER_FROM_DB: %w", err) + } + + return &user, nil +} + +func (p *pg) GetUserById(ctx context.Context, userId string) (*types.User, error) { + childCtx, cancel := context.WithTimeout(ctx, time.Millisecond*100) + defer cancel() + + row := p.conn.QueryRow(childCtx, queries.GetUserById, userId) var user types.User err := row.Scan( @@ -77,12 +116,33 @@ func (p *pg) GetUser(ctx context.Context, identifier string) (*types.User, error &user.IsActive, &user.Username, &user.Email, - &user.Password, &user.CreatedAt, &user.UpdatedAt, ) if err != nil { - return nil, fmt.Errorf("user not found") + return nil, fmt.Errorf("ERR_SESSION_NOT_FOUND: %w", err) + } + + return &user, nil +} + +func (p *pg) GetUserWithSession(ctx context.Context, sessionId string) (*types.User, error) { + childCtx, cancel := context.WithTimeout(ctx, time.Millisecond*100) + defer cancel() + + row := p.conn.QueryRow(childCtx, queries.GetUserWithSession, sessionId) + + var user types.User + if err := row.Scan( + &user.Id, + &user.IsActive, + &user.Name, + &user.Username, + &user.Email, + &user.CreatedAt, + &user.UpdatedAt, + ); err != nil { + return nil, fmt.Errorf("ERR_SESSION_NOT_FOUND: %w", err) } return &user, nil @@ -125,3 +185,15 @@ func (p *pg) IsActive(ctx context.Context, identifier string) bool { row := p.conn.QueryRow(childCtx, queries.GetUser, identifier) return row != nil } + +func (p *pg) UserExists(ctx context.Context, id string) bool { + childCtx, cancel := context.WithTimeout(ctx, time.Millisecond*100) + defer cancel() + + row, err := p.GetUserById(childCtx, id) + if err != nil || row == nil { + return false + } + + return true +} diff --git a/types/auth.go b/types/auth.go index a2037c7c..0c3dea31 100644 --- a/types/auth.go +++ b/types/auth.go @@ -1,22 +1,20 @@ package types -import "time" - type ( - // "id" uuid PRIMARY KEY, - // "is_active" boolean, - // "created_at" timestamp, - // "updated_at" timestamp, - // "expired_at" timestamp, - // "expires_at" timestamp, - // "refresh_token" text - Session struct { - CreatedAt time.Time - UpdatedAt time.Time - ExpiredAt time.Time - ExpiresAt time.Time - ID string - RefreshToken string - IsActive bool - } +// "id" uuid PRIMARY KEY, +// "is_active" boolean, +// "created_at" timestamp, +// "updated_at" timestamp, +// "expired_at" timestamp, +// "expires_at" timestamp, +// "refresh_token" text +//Session struct { +// CreatedAt time.Time +// UpdatedAt time.Time +// ExpiredAt time.Time +// ExpiresAt time.Time +// ID string +// RefreshToken string +// IsActive bool +//} ) diff --git a/types/types.go b/types/types.go index f8594b8c..125a64a1 100644 --- a/types/types.go +++ b/types/types.go @@ -3,6 +3,7 @@ package types import ( "encoding/json" "fmt" + "time" ) type ( @@ -19,13 +20,16 @@ type ( } ImageManifestV2 struct { - Uuid string `json:"uuid"` - Namespace string `json:"namespace"` - MediaType string `json:"mediaType"` - SchemaVersion int `json:"schemaVersion"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + Uuid string `json:"uuid,omitempty"` + Namespace string `json:"namespace"` + MediaType string `json:"mediaType,omitempty"` + SchemaVersion int `json:"schemaVersion,omitempty"` } Blob struct { + CreatedAt time.Time Digest string Skylink string UUID string @@ -43,12 +47,14 @@ type ( } LayerV2 struct { - MediaType string `json:"mediaType"` - Digest string `json:"digest"` - SkynetLink string `json:"skynetLink"` - UUID string `json:"uuid"` - BlobDigests []string `json:"blobs"` - Size int `json:"size"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + SkynetLink string `json:"skynetLink"` + UUID string `json:"uuid"` + BlobDigests []string `json:"blobs"` + Size int `json:"size"` } LayerRef struct { @@ -65,14 +71,24 @@ type ( } ConfigV2 struct { - UUID string `json:"uuid"` - Namespace string `json:"namespace"` - Reference string `json:"reference"` - Digest string `json:"digest"` - Skylink string `json:"sky_link,omitempty"` - MediaType string `json:"media_type,omitempty"` - Layers []string `json:"layers,omitempty"` - Size int `json:"size,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UUID string `json:"uuid,omitempty"` + Namespace string `json:"namespace,omitempty"` + Skylink string `json:"sky_link,omitempty"` + MediaType string `json:"media_type,omitempty"` + Reference string `json:"reference"` + Digest string `json:"digest"` + Layers []string `json:"layers,omitempty"` + Size int `json:"size,omitempty"` + } + Catalog struct { + Repositories []*Repository `json:"repositories"` + } + + Repository struct { + Namespace string `json:"namespace"` + Tags []*ConfigV2 `json:"tags"` } ) diff --git a/types/users.go b/types/users.go index b5051c91..a75586cb 100644 --- a/types/users.go +++ b/types/users.go @@ -57,6 +57,11 @@ type ( ID int `json:"id"` Hireable bool `json:"hireable"` } + Session struct { + Id string `json:"id"` + RefreshToken string `json:"refresh_token"` + Owner string `json:"-"` + } ) func (u *User) Validate() error {