From 7ce29cf5276e0c2433e4955e27438be9bf88b272 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Wed, 16 Feb 2022 13:36:00 +0530 Subject: [PATCH 01/10] Add: Added Additional Fields for Cookies --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index ba1fbe23..4433241a 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 ) + require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect From b3fabee478ba08a2c41b4e2e7d07f3e00d63fffe Mon Sep 17 00:00:00 2001 From: guacamole Date: Sun, 20 Feb 2022 18:01:58 +0530 Subject: [PATCH 02/10] Feat: refresh token --- auth/refresh.go | 1 + auth/sessions.go | 22 ++++++++ .../000006_create_session_table.down.sql | 1 + .../000006_create_session_table.up.sql | 5 ++ store/postgres/postgres.go | 4 ++ store/postgres/queries/users.go | 7 +++ store/postgres/sessions.go | 55 +++++++++++++++++++ types/users.go | 5 ++ 8 files changed, 100 insertions(+) create mode 100644 auth/refresh.go create mode 100644 auth/sessions.go create mode 100644 db/migrations/000006_create_session_table.down.sql create mode 100644 db/migrations/000006_create_session_table.up.sql create mode 100644 store/postgres/sessions.go diff --git a/auth/refresh.go b/auth/refresh.go new file mode 100644 index 00000000..8832b06d --- /dev/null +++ b/auth/refresh.go @@ -0,0 +1 @@ +package auth diff --git a/auth/sessions.go b/auth/sessions.go new file mode 100644 index 00000000..49dd80cd --- /dev/null +++ b/auth/sessions.go @@ -0,0 +1,22 @@ +package auth + +import ( + "github.com/labstack/echo/v4" + "net/http" + "strconv" +) + +func (a *auth) ExpireSessions(ctx echo.Context) error { + sessionId := ctx.QueryParam("session_id") + deleteAll, err := strconv.ParseBool(ctx.QueryParam("delete_all")) + if err != nil { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "delete_all must be a boolean", + }) + } + if sessionId != "" { + + } + return nil +} 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..8575ba3a --- /dev/null +++ b/db/migrations/000006_create_session_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "session" ( + "id" uuid PRIMARY KEY, + "refresh_token" text UNIQUE NOT NULL, + "owner" uuid references users(id), +); diff --git a/store/postgres/postgres.go b/store/postgres/postgres.go index aa8b20ec..2ddde713 100644 --- a/store/postgres/postgres.go +++ b/store/postgres/postgres.go @@ -46,6 +46,10 @@ type UserStore interface { 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 + 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 RegistryStore interface { diff --git a/store/postgres/queries/users.go b/store/postgres/queries/users.go index f353ec4d..4dff99c8 100644 --- a/store/postgres/queries/users.go +++ b/store/postgres/queries/users.go @@ -13,3 +13,10 @@ values ($1, $2, $3, $4, $5, $6, $7);` 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, $3);` + GetSession = `select * 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..2499620b --- /dev/null +++ b/store/postgres/sessions.go @@ -0,0 +1,55 @@ +package postgres + +import ( + "context" + "fmt" + "github.com/containerish/OpenRegistry/store/postgres/queries" + "github.com/containerish/OpenRegistry/types" + "github.com/jackc/pgx/v4" + "time" +) + +func (p *pg) AddSession(ctx context.Context, id, refreshToken, owner string) error { + childCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + _, err := p.conn.Exec(childCtx, queries.AddSession, id, refreshToken, owner) + 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/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 { From 021908f79b1c03e8c5ed78922a5d07de24873301 Mon Sep 17 00:00:00 2001 From: guacamole Date: Mon, 28 Feb 2022 10:56:14 +0530 Subject: [PATCH 03/10] Add: refresh token --- auth/auth.go | 3 + auth/github.go | 83 +++++++++++-------- auth/jwt.go | 4 +- auth/jwt_middleware.go | 10 ++- auth/sessions.go | 40 +++++++-- auth/signout.go | 33 ++++++++ auth/user.go | 35 ++++++++ .../000006_create_session_table.up.sql | 4 +- .../000006_create_sessions_table.down.sql | 1 - .../000006_create_sessions_table.up.sql | 9 -- router/helpers.go | 8 ++ store/postgres/postgres.go | 10 ++- store/postgres/queries/auth.go | 10 --- store/postgres/queries/users.go | 16 ++-- store/postgres/sessions.go | 4 +- store/postgres/users.go | 21 +++++ types/auth.go | 34 ++++---- 17 files changed, 227 insertions(+), 98 deletions(-) create mode 100644 auth/signout.go create mode 100644 auth/user.go delete mode 100644 db/migrations/000006_create_sessions_table.down.sql delete mode 100644 db/migrations/000006_create_sessions_table.up.sql diff --git a/auth/auth.go b/auth/auth.go index 6870d55e..bbdf425f 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -23,6 +23,9 @@ type Authentication interface { 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 } // New is the constructor function returns an Authentication implementation diff --git a/auth/github.go b/auth/github.go index ea4084e4..df87219f 100644 --- a/auth/github.go +++ b/auth/github.go @@ -2,11 +2,12 @@ package auth import ( "context" + "fmt" + "github.com/containerish/OpenRegistry/config" + "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,39 +79,7 @@ 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, - } - + 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{ @@ -119,8 +88,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 +112,33 @@ 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, + MaxAge: AccessCookieMaxAge, + Secure: secure, + SameSite: sameSite, + HttpOnly: httpOnly, + } + return cookie +} diff --git a/auth/jwt.go b/auth/jwt.go index 01f673f9..53f771b0 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -164,7 +164,7 @@ func (a *auth) createRefreshClaims(u types.User) RefreshClaims { StandardClaims: jwt.StandardClaims{ Audience: a.c.Endpoint(), ExpiresAt: time.Now().Add(time.Hour * 750).Unix(), // Refresh tokens can live longer - Id: uuid.NewString(), + Id: u.Id, IssuedAt: time.Now().Unix(), Issuer: a.c.Endpoint(), NotBefore: time.Now().Unix(), @@ -181,7 +181,7 @@ func (a *auth) createWebLoginClaims(u types.User) PlatformClaims { StandardClaims: jwt.StandardClaims{ Audience: a.c.Endpoint(), ExpiresAt: time.Now().Add(time.Hour).Unix(), - Id: uuid.NewString(), + Id: u.Id, IssuedAt: time.Now().Unix(), Issuer: a.c.Endpoint(), NotBefore: time.Now().Unix(), diff --git a/auth/jwt_middleware.go b/auth/jwt_middleware.go index 421f39f6..e49c3170 100644 --- a/auth/jwt_middleware.go +++ b/auth/jwt_middleware.go @@ -29,12 +29,16 @@ 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) + ctx.Set(types.HttpEndpointErrorKey, err.Error()) + a.logger.Log(ctx, nil) + return ctx.JSON(http.StatusUnauthorized, echo.Map{ + "error": err.Error(), + "message": "missing authentication information", + }) }, KeyFunc: middleware.DefaultJWTConfig.KeyFunc, ParseTokenFunc: middleware.DefaultJWTConfig.ParseTokenFunc, diff --git a/auth/sessions.go b/auth/sessions.go index 49dd80cd..faf3e63b 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -1,22 +1,46 @@ package auth import ( + "github.com/fatih/color" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" "github.com/labstack/echo/v4" "net/http" "strconv" ) func (a *auth) ExpireSessions(ctx echo.Context) error { - sessionId := ctx.QueryParam("session_id") - deleteAll, err := strconv.ParseBool(ctx.QueryParam("delete_all")) - if err != nil { - return ctx.JSON(http.StatusBadRequest, echo.Map{ - "error": err.Error(), - "message": "delete_all must be a boolean", - }) + queryParamSessionId := ctx.QueryParam("session_id") + var sessionID uuid.UUID + var err error + if queryParamSessionId != "" { + sessionID, err = uuid.Parse(queryParamSessionId) + if err != nil { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "invalid session id", + }) + } } - if sessionId != "" { + 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", + }) + } + } + + if deleteAllSessions { + user := ctx.Get("user").(*jwt.Token) + claims := user.Claims.(*Claims) + userId := claims.Id + color.Red("came here, userId:%s:%s", sessionID, userId) } + return nil } diff --git a/auth/signout.go b/auth/signout.go new file mode 100644 index 00000000..0627716a --- /dev/null +++ b/auth/signout.go @@ -0,0 +1,33 @@ +package auth + +import ( + "github.com/labstack/echo/v4" + "net/http" + "strings" + "time" +) + +func (a *auth) SignOut(ctx echo.Context) error { + parts := strings.Split(ctx.QueryParam("session_id"), ":") + 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("session_id", "", true, time.Now().Add(-time.Hour))) + ctx.SetCookie(a.createCookie("refresh", "", true, time.Now().Add(-time.Hour))) + return ctx.JSON(http.StatusAccepted, echo.Map{ + "message": "session deleted successfully", + }) +} diff --git a/auth/user.go b/auth/user.go new file mode 100644 index 00000000..bb1cb067 --- /dev/null +++ b/auth/user.go @@ -0,0 +1,35 @@ +package auth + +import ( + "fmt" + "github.com/labstack/echo/v4" + "net/http" + "strings" +) + +func (a *auth) ReadUserWithSession(ctx echo.Context) error { + cookie, err := ctx.Cookie("session_id") + if err != nil { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "msg": "error is cookie", + }) + } + parts := strings.Split(cookie.Value, ":") + if len(parts) != 2 { + 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 { + return ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + "message": "ERROR_FETCHING_USER_WITH_SESSION", + }) + } + fmt.Printf("session details: id: %s owner:%s", user.Id, user.Username) + return ctx.JSON(http.StatusOK, user) +} diff --git a/db/migrations/000006_create_session_table.up.sql b/db/migrations/000006_create_session_table.up.sql index 8575ba3a..463bf89d 100644 --- a/db/migrations/000006_create_session_table.up.sql +++ b/db/migrations/000006_create_session_table.up.sql @@ -1,5 +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), + "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/router/helpers.go b/router/helpers.go index 3c48cd66..c88c3190 100644 --- a/router/helpers.go +++ b/router/helpers.go @@ -16,7 +16,15 @@ 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.MethodDelete, "/signout", authSvc.SignOut) authRouter.Add(http.MethodPost, "/token", authSvc.SignIn) + authRouter.Add(http.MethodGet, "/sessions/me", authSvc.ReadUserWithSession) + authRouter.Add(http.MethodDelete, "/sessions", authSvc.ExpireSessions, func(hf echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + ctx.Set("JWT_AUTH", true) + return hf(ctx) + } + }, authSvc.JWT()) } diff --git a/store/postgres/postgres.go b/store/postgres/postgres.go index 2ddde713..3a5d0490 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) @@ -47,7 +48,7 @@ type UserStore interface { DeleteUser(ctx context.Context, identifier string) error IsActive(ctx context.Context, identifier string) bool AddSession(ctx context.Context, sessionId, refreshToken, owner string) error - GetSession(ctx context.Context, sessionId string) (*types.Session, error) + GetUserWithSession(ctx context.Context, sessionId string) (*types.User, error) DeleteSession(ctx context.Context, sessionId, userId string) error DeleteAllSessions(ctx context.Context, userId string) error } @@ -74,6 +75,13 @@ type RegistryStore interface { 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/users.go b/store/postgres/queries/users.go index 4dff99c8..4ab0b7b3 100644 --- a/store/postgres/queries/users.go +++ b/store/postgres/queries/users.go @@ -4,19 +4,19 @@ 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, + GetUser = `select id, is_active, username, email, password, created_at, updated_at from users where email=$1 or username=$1;` + GetUserWithSession = `select id, is_active, 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, password, 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, $3);` - GetSession = `select * from session where id=$1;` + 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 index 2499620b..c945a535 100644 --- a/store/postgres/sessions.go +++ b/store/postgres/sessions.go @@ -9,11 +9,11 @@ import ( "time" ) -func (p *pg) AddSession(ctx context.Context, id, refreshToken, owner string) error { +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, owner) + _, err := p.conn.Exec(childCtx, queries.AddSession, id, refreshToken, username) if err != nil { return fmt.Errorf("ERR_CREATE_SESSION: %w", err) } diff --git a/store/postgres/users.go b/store/postgres/users.go index 01c26e12..74e85d96 100644 --- a/store/postgres/users.go +++ b/store/postgres/users.go @@ -87,6 +87,27 @@ func (p *pg) GetUser(ctx context.Context, identifier string) (*types.User, error 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 + err := row.Scan( + &user.Id, + &user.IsActive, + &user.Username, + &user.Email, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("user not found") + } + + return &user, nil +} // UpdateUser //update user set username = $1, email = $2, password = $3, updated_at = $4 where username = $5 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 +//} ) From 45af66e5ec95956b86499faf0fabde8d2b83b87e Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Mon, 7 Mar 2022 11:28:31 +0530 Subject: [PATCH 04/10] Fix: Session Cookies --- auth/github.go | 6 +++--- auth/signin.go | 35 +++++++++++++++++---------------- auth/signout.go | 2 +- auth/user.go | 26 ++++++++++++++++-------- store/postgres/queries/users.go | 2 +- store/postgres/users.go | 1 + 6 files changed, 42 insertions(+), 30 deletions(-) diff --git a/auth/github.go b/auth/github.go index df87219f..1749ffc1 100644 --- a/auth/github.go +++ b/auth/github.go @@ -3,11 +3,12 @@ package auth import ( "context" "fmt" - "github.com/containerish/OpenRegistry/config" "net" "net/http" "time" + "github.com/containerish/OpenRegistry/config" + "github.com/containerish/OpenRegistry/types" "github.com/google/uuid" "github.com/labstack/echo/v4" @@ -80,7 +81,7 @@ func (a *auth) GithubLoginCallbackHandler(ctx echo.Context) error { } oauthUser.Password = refreshToken - if err := a.pgStore.AddOAuthUser(ctx.Request().Context(), &oauthUser); err != nil { + 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(), @@ -135,7 +136,6 @@ func (a *auth) createCookie(name string, value string, httpOnly bool, expiresAt Path: "/", Domain: webappEndpoint, Expires: expiresAt, - MaxAge: AccessCookieMaxAge, Secure: secure, SameSite: sameSite, HttpOnly: httpOnly, diff --git a/auth/signin.go b/auth/signin.go index f2e5d49e..32f83091 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(), }) @@ -56,27 +58,26 @@ func (a *auth) SignIn(ctx echo.Context) error { }) } - accessCookie := &http.Cookie{ - Name: "access", - Value: access, - Expires: time.Now().Add(time.Hour), - 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", + }) } + 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)) - refreshCookie := &http.Cookie{ - Name: "refresh", - Value: refresh, - Expires: time.Now().Add(time.Hour * 750), - Path: "/", - } - http.SetCookie(ctx.Response(), accessCookie) - http.SetCookie(ctx.Response(), refreshCookie) 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 index 0627716a..07482e0a 100644 --- a/auth/signout.go +++ b/auth/signout.go @@ -25,8 +25,8 @@ func (a *auth) SignOut(ctx echo.Context) error { } ctx.SetCookie(a.createCookie("access", "", true, time.Now().Add(-time.Hour))) - ctx.SetCookie(a.createCookie("session_id", "", 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/user.go b/auth/user.go index bb1cb067..50fbfb71 100644 --- a/auth/user.go +++ b/auth/user.go @@ -1,35 +1,45 @@ package auth import ( - "fmt" - "github.com/labstack/echo/v4" "net/http" "strings" + "time" + + "github.com/containerish/OpenRegistry/types" + "github.com/fatih/color" + "github.com/labstack/echo/v4" ) func (a *auth) ReadUserWithSession(ctx echo.Context) error { - cookie, err := ctx.Cookie("session_id") - if err != nil { + ctx.Set(types.HandlerStartTime, time.Now()) + defer func() { + a.logger.Log(ctx).Send() + }() + + cookie := ctx.QueryParam("session_id") + if cookie == "" { + ctx.Set(types.HttpEndpointErrorKey, "error in getting cookies") return ctx.JSON(http.StatusBadRequest, echo.Map{ - "error": err.Error(), - "msg": "error is cookie", + "msg": "error is cookie", }) } - parts := strings.Split(cookie.Value, ":") + parts := strings.Split(cookie, ":") 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] + color.Yellow("sessin in getSesion: %s", sessionId) 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", }) } - fmt.Printf("session details: id: %s owner:%s", user.Id, user.Username) return ctx.JSON(http.StatusOK, user) } diff --git a/store/postgres/queries/users.go b/store/postgres/queries/users.go index 4ab0b7b3..af7b5baa 100644 --- a/store/postgres/queries/users.go +++ b/store/postgres/queries/users.go @@ -5,7 +5,7 @@ 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;` - GetUserWithSession = `select id, is_active, username, email, created_at, updated_at from users where id=(select owner from session 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;` diff --git a/store/postgres/users.go b/store/postgres/users.go index 74e85d96..3b475fbe 100644 --- a/store/postgres/users.go +++ b/store/postgres/users.go @@ -97,6 +97,7 @@ func (p *pg) GetUserWithSession(ctx context.Context, sessionId string) (*types.U err := row.Scan( &user.Id, &user.IsActive, + &user.Name, &user.Username, &user.Email, &user.CreatedAt, From 1b4fcb9ee7b481e3dffa145bff005886c81f430e Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Mon, 14 Mar 2022 10:00:46 +0530 Subject: [PATCH 05/10] Fix: Cookies & Sessions --- auth/sessions.go | 1 - auth/signout.go | 4 +++- auth/user.go | 11 +++++------ store/postgres/queries/users.go | 4 ++-- store/postgres/users.go | 9 ++++----- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/auth/sessions.go b/auth/sessions.go index faf3e63b..ad0be210 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -39,7 +39,6 @@ func (a *auth) ExpireSessions(ctx echo.Context) error { claims := user.Claims.(*Claims) userId := claims.Id color.Red("came here, userId:%s:%s", sessionID, userId) - } return nil diff --git a/auth/signout.go b/auth/signout.go index 07482e0a..c76dc6f7 100644 --- a/auth/signout.go +++ b/auth/signout.go @@ -8,12 +8,14 @@ import ( ) func (a *auth) SignOut(ctx echo.Context) error { - parts := strings.Split(ctx.QueryParam("session_id"), ":") + sessionCookie, _ := ctx.Cookie("session_id") + 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] diff --git a/auth/user.go b/auth/user.go index 50fbfb71..e7c43ac7 100644 --- a/auth/user.go +++ b/auth/user.go @@ -6,7 +6,6 @@ import ( "time" "github.com/containerish/OpenRegistry/types" - "github.com/fatih/color" "github.com/labstack/echo/v4" ) @@ -16,23 +15,23 @@ func (a *auth) ReadUserWithSession(ctx echo.Context) error { a.logger.Log(ctx).Send() }() - cookie := ctx.QueryParam("session_id") - if cookie == "" { + session, _ := ctx.Cookie("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(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] - color.Yellow("sessin in getSesion: %s", sessionId) + sessionId := parts[0] user, err := a.pgStore.GetUserWithSession(ctx.Request().Context(), sessionId) if err != nil { ctx.Set(types.HttpEndpointErrorKey, err.Error()) diff --git a/store/postgres/queries/users.go b/store/postgres/queries/users.go index af7b5baa..c799f815 100644 --- a/store/postgres/queries/users.go +++ b/store/postgres/queries/users.go @@ -2,8 +2,8 @@ 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);` + 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, password, created_at, updated_at from users where email=$1 or username=$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;` diff --git a/store/postgres/users.go b/store/postgres/users.go index 3b475fbe..2579b159 100644 --- a/store/postgres/users.go +++ b/store/postgres/users.go @@ -20,7 +20,7 @@ func (p *pg) AddUser(ctx context.Context, u *types.User) error { 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, id.String(), true, u.Username, u.Name, u.Email, u.Password, t, t) if err != nil { return fmt.Errorf("error adding user to database: %w", err) } @@ -94,7 +94,7 @@ func (p *pg) GetUserWithSession(ctx context.Context, sessionId string) (*types.U row := p.conn.QueryRow(childCtx, queries.GetUserWithSession, sessionId) var user types.User - err := row.Scan( + if err := row.Scan( &user.Id, &user.IsActive, &user.Name, @@ -102,9 +102,8 @@ func (p *pg) GetUserWithSession(ctx context.Context, sessionId string) (*types.U &user.Email, &user.CreatedAt, &user.UpdatedAt, - ) - if err != nil { - return nil, fmt.Errorf("user not found") + ); err != nil { + return nil, fmt.Errorf("ERR_SESSION_NOT_FOUND: %w", err) } return &user, nil From 586d740418187e386afd69537f1afddeae4805b3 Mon Sep 17 00:00:00 2001 From: guacamole Date: Wed, 16 Mar 2022 15:54:47 +0530 Subject: [PATCH 06/10] Fix: error handling and functioning of session apis --- auth/sessions.go | 64 ++++++++++++++++++++++++++++++++++------------- auth/signin.go | 1 - auth/signout.go | 10 +++++++- auth/user.go | 9 ++++++- router/helpers.go | 8 +----- 5 files changed, 64 insertions(+), 28 deletions(-) diff --git a/auth/sessions.go b/auth/sessions.go index ad0be210..649ab822 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -1,27 +1,27 @@ package auth import ( - "github.com/fatih/color" - "github.com/golang-jwt/jwt" "github.com/google/uuid" "github.com/labstack/echo/v4" "net/http" "strconv" + "strings" ) func (a *auth) ExpireSessions(ctx echo.Context) error { - queryParamSessionId := ctx.QueryParam("session_id") - var sessionID uuid.UUID - var err error - if queryParamSessionId != "" { - sessionID, err = uuid.Parse(queryParamSessionId) - if err != nil { - return ctx.JSON(http.StatusBadRequest, echo.Map{ - "error": err.Error(), - "message": "invalid session id", - }) - } + //queryParamSessionId := ctx.QueryParam("session_id") + cookie, _ := ctx.Cookie("session_id") + 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 err error + var deleteAllSessions bool queryParamDeleteAll := ctx.QueryParam("delete_all") if queryParamDeleteAll != "" { @@ -32,13 +32,41 @@ func (a *auth) ExpireSessions(ctx echo.Context) 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 deleteAllSessions { - user := ctx.Get("user").(*jwt.Token) - claims := user.Claims.(*Claims) - userId := claims.Id - color.Red("came here, userId:%s:%s", sessionID, userId) + 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 32f83091..0080e37f 100644 --- a/auth/signin.go +++ b/auth/signin.go @@ -35,7 +35,6 @@ 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) if err != nil { a.logger.Log(ctx, err) diff --git a/auth/signout.go b/auth/signout.go index c76dc6f7..8eb40e7b 100644 --- a/auth/signout.go +++ b/auth/signout.go @@ -1,6 +1,7 @@ package auth import ( + "github.com/containerish/OpenRegistry/types" "github.com/labstack/echo/v4" "net/http" "strings" @@ -8,7 +9,14 @@ import ( ) func (a *auth) SignOut(ctx echo.Context) error { - sessionCookie, _ := ctx.Cookie("session_id") + 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{ diff --git a/auth/user.go b/auth/user.go index e7c43ac7..cb7c4149 100644 --- a/auth/user.go +++ b/auth/user.go @@ -15,7 +15,14 @@ func (a *auth) ReadUserWithSession(ctx echo.Context) error { a.logger.Log(ctx).Send() }() - session, _ := ctx.Cookie("session_id") + 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{ diff --git a/router/helpers.go b/router/helpers.go index c88c3190..3e2886c6 100644 --- a/router/helpers.go +++ b/router/helpers.go @@ -19,13 +19,7 @@ func RegisterAuthRoutes(authRouter *echo.Group, authSvc auth.Authentication) { authRouter.Add(http.MethodDelete, "/signout", authSvc.SignOut) authRouter.Add(http.MethodPost, "/token", authSvc.SignIn) authRouter.Add(http.MethodGet, "/sessions/me", authSvc.ReadUserWithSession) - authRouter.Add(http.MethodDelete, "/sessions", authSvc.ExpireSessions, func(hf echo.HandlerFunc) echo.HandlerFunc { - return func(ctx echo.Context) error { - ctx.Set("JWT_AUTH", true) - return hf(ctx) - } - }, authSvc.JWT()) - + authRouter.Add(http.MethodDelete, "/sessions", authSvc.ExpireSessions, authSvc.JWT()) } // RegisterBetaRoutes contains the experimental features, the betas From 38d2906742dfe8c2686c0c66e38bbcfc5d845a06 Mon Sep 17 00:00:00 2001 From: guacamole Date: Fri, 18 Mar 2022 12:20:50 +0530 Subject: [PATCH 07/10] Add: renew access token implemented --- auth/auth.go | 2 + auth/jwt.go | 92 ++++++++++++++++++++------------- auth/jwt_middleware.go | 25 +++++++++ auth/refresh.go | 1 - auth/renew.go | 68 ++++++++++++++++++++++++ auth/sessions.go | 11 ++-- auth/signin.go | 4 +- router/helpers.go | 7 +-- store/postgres/postgres.go | 4 +- store/postgres/queries/users.go | 1 + store/postgres/users.go | 36 +++++++++++++ 11 files changed, 205 insertions(+), 46 deletions(-) delete mode 100644 auth/refresh.go create mode 100644 auth/renew.go diff --git a/auth/auth.go b/auth/auth.go index bbdf425f..5b3e2dca 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -20,12 +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/jwt.go b/auth/jwt.go index 53f771b0..b82eb5d2 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -13,12 +13,13 @@ import ( type Claims struct { jwt.StandardClaims Access AccessList + Type string } 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: fmt.Sprintf("*/*"), + 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)) @@ -87,25 +96,22 @@ func (a *auth) newServiceToken(u types.User) (string, error) { 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 +148,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,17 +163,17 @@ 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: u.Id, + Id: userId, IssuedAt: time.Now().Unix(), Issuer: a.c.Endpoint(), NotBefore: time.Now().Unix(), - Subject: u.Id, + Subject: userId, }, } @@ -177,10 +182,9 @@ func (a *auth) createRefreshClaims(u types.User) RefreshClaims { 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(), + ExpiresAt: time.Now().Add(time.Minute).Unix(), Id: u.Id, IssuedAt: time.Now().Unix(), Issuer: a.c.Endpoint(), @@ -195,7 +199,15 @@ func (a *auth) createWebLoginClaims(u types.User) PlatformClaims { func (a *auth) newToken(u types.User, tokenLife int64) (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 +243,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 e49c3170..3b4d9843 100644 --- a/auth/jwt_middleware.go +++ b/auth/jwt_middleware.go @@ -85,3 +85,28 @@ func (a *auth) ACL() echo.MiddlewareFunc { } } } + +// 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) + 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/refresh.go b/auth/refresh.go deleted file mode 100644 index 8832b06d..00000000 --- a/auth/refresh.go +++ /dev/null @@ -1 +0,0 @@ -package auth diff --git a/auth/renew.go b/auth/renew.go new file mode 100644 index 00000000..1919fbbe --- /dev/null +++ b/auth/renew.go @@ -0,0 +1,68 @@ +package auth + +import ( + "github.com/golang-jwt/jwt" + "github.com/labstack/echo/v4" + "net/http" + "time" +) + +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": err.Error(), + "message": "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 index 649ab822..af499df8 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -10,17 +10,22 @@ import ( func (a *auth) ExpireSessions(ctx echo.Context) error { //queryParamSessionId := ctx.QueryParam("session_id") - cookie, _ := ctx.Cookie("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 err error var deleteAllSessions bool queryParamDeleteAll := ctx.QueryParam("delete_all") diff --git a/auth/signin.go b/auth/signin.go index 0080e37f..bae90ace 100644 --- a/auth/signin.go +++ b/auth/signin.go @@ -49,7 +49,9 @@ 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") + refresh, err := a.newWebLoginToken(userFromDb.Id, userFromDb.Username, "refresh") + if err != nil { a.logger.Log(ctx, err) return ctx.JSON(http.StatusInternalServerError, echo.Map{ diff --git a/router/helpers.go b/router/helpers.go index 3e2886c6..db2cc369 100644 --- a/router/helpers.go +++ b/router/helpers.go @@ -16,10 +16,11 @@ 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.MethodDelete, "/signout", authSvc.SignOut) authRouter.Add(http.MethodPost, "/token", authSvc.SignIn) - authRouter.Add(http.MethodGet, "/sessions/me", authSvc.ReadUserWithSession) - authRouter.Add(http.MethodDelete, "/sessions", authSvc.ExpireSessions, authSvc.JWT()) + authRouter.Add(http.MethodDelete, "/signout", authSvc.SignOut, authSvc.JWTRest()) + authRouter.Add(http.MethodGet, "/sessions/me", authSvc.ReadUserWithSession, authSvc.JWTRest()) + authRouter.Add(http.MethodDelete, "/sessions", authSvc.ExpireSessions) + authRouter.Add(http.MethodGet, "/renew", authSvc.RenewAccessToken) } // RegisterBetaRoutes contains the experimental features, the betas diff --git a/store/postgres/postgres.go b/store/postgres/postgres.go index 3a5d0490..c7e7f89f 100644 --- a/store/postgres/postgres.go +++ b/store/postgres/postgres.go @@ -43,12 +43,14 @@ type AuthStore interface { type UserStore interface { AddUser(ctx context.Context, u *types.User) error AddOAuthUser(ctx context.Context, u *types.User) error + UserExists(ctx context.Context, id string) bool GetUser(ctx context.Context, identifier string) (*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 - GetUserWithSession(ctx context.Context, sessionId string) (*types.User, error) DeleteSession(ctx context.Context, sessionId, userId string) error DeleteAllSessions(ctx context.Context, userId string) error } diff --git a/store/postgres/queries/users.go b/store/postgres/queries/users.go index c799f815..5978476a 100644 --- a/store/postgres/queries/users.go +++ b/store/postgres/queries/users.go @@ -5,6 +5,7 @@ var ( 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, password, created_at, updated_at from users where email=$1 or username=$1;` + GetUserById = `select id, is_active, username, email, password, 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;` diff --git a/store/postgres/users.go b/store/postgres/users.go index 2579b159..97a4a32f 100644 --- a/store/postgres/users.go +++ b/store/postgres/users.go @@ -87,6 +87,30 @@ func (p *pg) GetUser(ctx context.Context, identifier string) (*types.User, error 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( + &user.Id, + &user.IsActive, + &user.Username, + &user.Email, + &user.Password, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("user not found") + } + + 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() @@ -146,3 +170,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 +} From 897a59c5081f38555987eba65e9fd9dfce6b8b8c Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Sat, 19 Mar 2022 23:26:35 +0530 Subject: [PATCH 08/10] Fix: ACL Middleware and Token Endpoint --- auth/jwt_middleware.go | 8 +++++--- auth/validate_user.go | 6 ++---- skynet/skynet.go | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/auth/jwt_middleware.go b/auth/jwt_middleware.go index 3b4d9843..5e8b7f0f 100644 --- a/auth/jwt_middleware.go +++ b/auth/jwt_middleware.go @@ -75,9 +75,11 @@ func (a *auth) ACL() echo.MiddlewareFunc { return ctx.NoContent(http.StatusUnauthorized) } - username := ctx.Param("username") - if claims.Subject == username { - return hf(ctx) + if user, err := a.pgStore.GetUserById(ctx.Request().Context(), claims.Id); err == nil { + username := ctx.Param("username") + if user.Username == username { + return hf(ctx) + } } a.logger.Log(ctx, fmt.Errorf("ACL: username didn't match from token")) diff --git a/auth/validate_user.go b/auth/validate_user.go index 86610ef1..d8d4919e 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" ) @@ -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/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() From 81c258c3be88734e33f1393daf7f26c09f58b956 Mon Sep 17 00:00:00 2001 From: guacamole Date: Sun, 20 Mar 2022 12:42:16 +0530 Subject: [PATCH 09/10] Add: docker login with github token id now funtional - user can generate a personal github token and use it as password for docker login - github o_auth token can be used in the same way --- auth/basic_auth.go | 15 ++++++------ auth/jwt.go | 16 ++++++++---- auth/jwt_middleware.go | 24 +++++++++--------- auth/signin.go | 2 +- auth/signup.go | 43 +++++++++++++++++++++------------ auth/token.go | 20 ++++++++++++--- auth/user.go | 3 --- auth/validate_user.go | 2 +- go.mod | 1 - router/helpers.go | 4 +-- store/postgres/postgres.go | 2 +- store/postgres/queries/users.go | 15 ++++++------ store/postgres/users.go | 33 ++++++++++++++++++------- 13 files changed, 111 insertions(+), 69 deletions(-) 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/jwt.go b/auth/jwt.go index b82eb5d2..924e0abd 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -84,13 +84,19 @@ 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 @@ -196,7 +202,7 @@ func (a *auth) createWebLoginClaims(u types.User) PlatformClaims { 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 diff --git a/auth/jwt_middleware.go b/auth/jwt_middleware.go index 5e8b7f0f..1d4fbede 100644 --- a/auth/jwt_middleware.go +++ b/auth/jwt_middleware.go @@ -33,8 +33,7 @@ func (a *auth) JWT() echo.MiddlewareFunc { 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, nil) + a.logger.Log(ctx, err) return ctx.JSON(http.StatusUnauthorized, echo.Map{ "error": err.Error(), "message": "missing authentication information", @@ -54,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 { @@ -75,15 +71,19 @@ func (a *auth) ACL() echo.MiddlewareFunc { return ctx.NoContent(http.StatusUnauthorized) } - if user, err := a.pgStore.GetUserById(ctx.Request().Context(), claims.Id); err == nil { - username := ctx.Param("username") - if user.Username == username { - return hf(ctx) - } + username := ctx.Param("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) + } } } @@ -98,7 +98,7 @@ func (a *auth) JWTRest() echo.MiddlewareFunc { // ErrorHandlerWithContext only logs the failing requtest ctx.Set(types.HandlerStartTime, time.Now()) ctx.Set(types.HttpEndpointErrorKey, err.Error()) - a.logger.Log(ctx) + a.logger.Log(ctx, err) return ctx.JSON(http.StatusUnauthorized, echo.Map{ "error": err.Error(), "message": "missing authentication information", diff --git a/auth/signin.go b/auth/signin.go index bae90ace..8da72a8d 100644 --- a/auth/signin.go +++ b/auth/signin.go @@ -35,7 +35,7 @@ func (a *auth) SignIn(ctx echo.Context) error { key = user.Username } - 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{ 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 index cb7c4149..b8cf9222 100644 --- a/auth/user.go +++ b/auth/user.go @@ -11,9 +11,6 @@ import ( func (a *auth) ReadUserWithSession(ctx echo.Context) error { ctx.Set(types.HandlerStartTime, time.Now()) - defer func() { - a.logger.Log(ctx).Send() - }() session, err := ctx.Cookie("session_id") if err != nil { diff --git a/auth/validate_user.go b/auth/validate_user.go index d8d4919e..7f9267e4 100644 --- a/auth/validate_user.go +++ b/auth/validate_user.go @@ -13,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 } diff --git a/go.mod b/go.mod index 4433241a..ba1fbe23 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 ) - require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/router/helpers.go b/router/helpers.go index db2cc369..f3edcba2 100644 --- a/router/helpers.go +++ b/router/helpers.go @@ -17,8 +17,8 @@ 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, authSvc.JWTRest()) - authRouter.Add(http.MethodGet, "/sessions/me", authSvc.ReadUserWithSession, authSvc.JWTRest()) + 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) } diff --git a/store/postgres/postgres.go b/store/postgres/postgres.go index c7e7f89f..48ace2ba 100644 --- a/store/postgres/postgres.go +++ b/store/postgres/postgres.go @@ -44,7 +44,7 @@ type UserStore interface { AddUser(ctx context.Context, u *types.User) error AddOAuthUser(ctx context.Context, u *types.User) error UserExists(ctx context.Context, id string) bool - GetUser(ctx context.Context, identifier string) (*types.User, error) + 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 diff --git a/store/postgres/queries/users.go b/store/postgres/queries/users.go index 5978476a..e3f23304 100644 --- a/store/postgres/queries/users.go +++ b/store/postgres/queries/users.go @@ -4,13 +4,14 @@ package queries var ( 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, password, created_at, updated_at from users where email=$1 or username=$1;` - GetUserById = `select id, is_active, username, email, password, 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, password, created_at, updated_at, + 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` ) diff --git a/store/postgres/users.go b/store/postgres/users.go index 97a4a32f..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.Name, 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,24 +64,41 @@ 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() - row := p.conn.QueryRow(childCtx, queries.GetUser, identifier) - 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.Password, &user.CreatedAt, &user.UpdatedAt, ) if err != nil { - return nil, fmt.Errorf("user not found") + return nil, fmt.Errorf("ERR_GET_USER_FROM_DB: %w", err) } return &user, nil @@ -100,12 +116,11 @@ func (p *pg) GetUserById(ctx context.Context, userId 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 From 94baf888ae68d5d7c230abf18b1d848d17dfd1ce Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Mon, 28 Mar 2022 20:13:20 +0530 Subject: [PATCH 10/10] Feat: Extensions APIs Signed-off-by: jay-dee7 --- Makefile | 6 + auth/jwt.go | 20 +- auth/renew.go | 10 +- auth/sessions.go | 5 +- auth/signin.go | 7 +- auth/signout.go | 5 +- db/migrations/000002_create_blob_table.up.sql | 12 +- .../000003_create_layer_table.up.sql | 14 +- .../000004_create_config_table.up.sql | 20 +- .../000005_create_image_manifest_table.up.sql | 11 +- main.go | 8 +- registry/v2/extensions/catalog_detail.go | 126 +++++++++++++ registry/v2/registry.go | 43 ++++- registry/v2/types.go | 24 +-- router/route_names.go | 13 +- router/router.go | 9 +- store/postgres/container_image.go | 172 ++++++++++++++++-- store/postgres/postgres.go | 6 +- store/postgres/queries/registry.go | 35 ++-- store/postgres/sessions.go | 3 +- types/types.go | 52 ++++-- 21 files changed, 470 insertions(+), 131 deletions(-) create mode 100644 registry/v2/extensions/catalog_detail.go 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/jwt.go b/auth/jwt.go index 924e0abd..efe861a1 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -12,8 +12,8 @@ import ( type Claims struct { jwt.StandardClaims - Access AccessList Type string + Access AccessList } type PlatformClaims struct { @@ -37,7 +37,7 @@ func (a *auth) newPublicPullToken() (string, error) { acl := AccessList{ { Type: "repository", - Name: fmt.Sprintf("*/*"), + Name: "*/*", Actions: []string{"pull"}, }, } @@ -186,22 +186,6 @@ func (a *auth) createRefreshClaims(userId string) RefreshClaims { return claims } -func (a *auth) createWebLoginClaims(u types.User) PlatformClaims { - claims := PlatformClaims{ - StandardClaims: jwt.StandardClaims{ - Audience: a.c.Endpoint(), - ExpiresAt: time.Now().Add(time.Minute).Unix(), - Id: u.Id, - IssuedAt: time.Now().Unix(), - Issuer: a.c.Endpoint(), - NotBefore: time.Now().Unix(), - Subject: u.Id, - }, - } - - return claims -} - 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 diff --git a/auth/renew.go b/auth/renew.go index 1919fbbe..0757eb01 100644 --- a/auth/renew.go +++ b/auth/renew.go @@ -1,14 +1,14 @@ package auth import ( - "github.com/golang-jwt/jwt" - "github.com/labstack/echo/v4" "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 { @@ -38,10 +38,10 @@ func (a *auth) RenewAccessToken(ctx echo.Context) error { "error": err.Error(), }) } + if !tkn.Valid { return ctx.JSON(http.StatusUnauthorized, echo.Map{ - "error": err.Error(), - "message": "invalid token, unauthorised", + "error": "invalid token, unauthorised", }) } diff --git a/auth/sessions.go b/auth/sessions.go index af499df8..54960373 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -1,11 +1,12 @@ package auth import ( - "github.com/google/uuid" - "github.com/labstack/echo/v4" "net/http" "strconv" "strings" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" ) func (a *auth) ExpireSessions(ctx echo.Context) error { diff --git a/auth/signin.go b/auth/signin.go index 8da72a8d..0bddedc4 100644 --- a/auth/signin.go +++ b/auth/signin.go @@ -50,8 +50,13 @@ func (a *auth) SignIn(ctx echo.Context) error { } 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(), + }) + } refresh, err := a.newWebLoginToken(userFromDb.Id, userFromDb.Username, "refresh") - if err != nil { a.logger.Log(ctx, err) return ctx.JSON(http.StatusInternalServerError, echo.Map{ diff --git a/auth/signout.go b/auth/signout.go index 8eb40e7b..167562d3 100644 --- a/auth/signout.go +++ b/auth/signout.go @@ -1,11 +1,12 @@ package auth import ( - "github.com/containerish/OpenRegistry/types" - "github.com/labstack/echo/v4" "net/http" "strings" "time" + + "github.com/containerish/OpenRegistry/types" + "github.com/labstack/echo/v4" ) func (a *auth) SignOut(ctx echo.Context) error { 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/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/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/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 48ace2ba..da141403 100644 --- a/store/postgres/postgres.go +++ b/store/postgres/postgres.go @@ -69,9 +69,11 @@ 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 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/sessions.go b/store/postgres/sessions.go index c945a535..9873de47 100644 --- a/store/postgres/sessions.go +++ b/store/postgres/sessions.go @@ -3,10 +3,11 @@ package postgres import ( "context" "fmt" + "time" + "github.com/containerish/OpenRegistry/store/postgres/queries" "github.com/containerish/OpenRegistry/types" "github.com/jackc/pgx/v4" - "time" ) func (p *pg) AddSession(ctx context.Context, id, refreshToken, username string) error { 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"` } )