Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
1d28cbcd92
commit
0d2f8be522
46 changed files with 1106 additions and 378 deletions
|
@ -47,7 +47,7 @@ const Api = Axios.create({
|
|||
baseURL: c.apiUri,
|
||||
headers: {
|
||||
common: {
|
||||
"X-Session-ID": window.localStorage.getItem("session_id"),
|
||||
"X-Session-ID": window.localStorage.getItem("authToken"),
|
||||
"X-Client-Uri": c.jsUri,
|
||||
"X-Client-Version": c.version,
|
||||
},
|
||||
|
|
|
@ -28,8 +28,9 @@ import Event from "pubsub-js";
|
|||
import User from "model/user";
|
||||
import Socket from "websocket.js";
|
||||
|
||||
const SessionHeader = "X-Session-ID";
|
||||
const PublicID = "234200000000000000000000000000000000000000000000";
|
||||
const RequestHeader = "X-Session-ID";
|
||||
const PublicSessionID = "a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f";
|
||||
const PublicAuthToken = "234200000000000000000000000000000000000000000000";
|
||||
const LoginPage = "login";
|
||||
|
||||
export default class Session {
|
||||
|
@ -39,7 +40,7 @@ export default class Session {
|
|||
* @param {object} shared
|
||||
*/
|
||||
constructor(storage, config, shared) {
|
||||
this.storage_key = "session_storage";
|
||||
this.storage_key = "sessionStorage";
|
||||
this.auth = false;
|
||||
this.config = config;
|
||||
this.user = new User(false);
|
||||
|
@ -52,9 +53,12 @@ export default class Session {
|
|||
this.storage = storage;
|
||||
}
|
||||
|
||||
// Restore from session storage.
|
||||
if (this.applyId(this.storage.getItem("session_id"))) {
|
||||
const dataJson = this.storage.getItem("data");
|
||||
// Restore authentication from session storage.
|
||||
if (
|
||||
this.applyAuthToken(this.storage.getItem("authToken")) &&
|
||||
this.applyId(this.storage.getItem("sessionId"))
|
||||
) {
|
||||
const dataJson = this.storage.getItem("sessionData");
|
||||
if (dataJson !== "undefined") {
|
||||
this.data = JSON.parse(dataJson);
|
||||
}
|
||||
|
@ -113,42 +117,91 @@ export default class Session {
|
|||
this.storage = window.localStorage;
|
||||
}
|
||||
|
||||
applyId(id) {
|
||||
if (!id) {
|
||||
setConfig(values) {
|
||||
this.config.setValues(values);
|
||||
}
|
||||
|
||||
setAuthToken(authToken) {
|
||||
if (authToken) {
|
||||
this.storage.setItem("authToken", authToken);
|
||||
if (authToken === PublicAuthToken) {
|
||||
this.setId(PublicSessionID);
|
||||
}
|
||||
}
|
||||
|
||||
return this.applyAuthToken(authToken);
|
||||
}
|
||||
|
||||
getAuthToken() {
|
||||
return this.authToken;
|
||||
}
|
||||
|
||||
hasAuthToken() {
|
||||
return !!this.authToken;
|
||||
}
|
||||
|
||||
applyAuthToken(authToken) {
|
||||
if (!authToken) {
|
||||
this.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.session_id = id;
|
||||
this.authToken = authToken;
|
||||
|
||||
Api.defaults.headers.common[SessionHeader] = id;
|
||||
Api.defaults.headers.common[RequestHeader] = authToken;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setId(id) {
|
||||
this.storage.setItem("session_id", id);
|
||||
return this.applyId(id);
|
||||
}
|
||||
|
||||
setConfig(values) {
|
||||
this.config.setValues(values);
|
||||
this.storage.setItem("sessionId", id);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.session_id;
|
||||
return this.id;
|
||||
}
|
||||
|
||||
hasId() {
|
||||
return !!this.session_id;
|
||||
return !!this.id;
|
||||
}
|
||||
|
||||
deleteId() {
|
||||
this.session_id = null;
|
||||
this.provider = "";
|
||||
this.storage.removeItem("session_id");
|
||||
applyId(id) {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete Api.defaults.headers.common[SessionHeader];
|
||||
this.setId(id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return this.hasId() && this.hasAuthToken();
|
||||
}
|
||||
|
||||
deleteAuthentication() {
|
||||
this.id = null;
|
||||
this.authToken = null;
|
||||
this.provider = "";
|
||||
this.storage.removeItem("sessionId");
|
||||
this.storage.removeItem("authToken");
|
||||
this.storage.removeItem("provider");
|
||||
|
||||
delete Api.defaults.headers.common[RequestHeader];
|
||||
}
|
||||
|
||||
setProvider(provider) {
|
||||
this.storage.setItem("provider", provider);
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
getProvider() {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
hasProvider() {
|
||||
return !!this.provider;
|
||||
}
|
||||
|
||||
setResp(resp) {
|
||||
|
@ -159,15 +212,25 @@ export default class Session {
|
|||
if (resp.data.id) {
|
||||
this.setId(resp.data.id);
|
||||
}
|
||||
if (resp.data.provider) {
|
||||
this.provider = resp.data.provider;
|
||||
|
||||
if (resp.data.access_token) {
|
||||
this.setAuthToken(resp.data.access_token);
|
||||
} else if (resp.data.id) {
|
||||
this.setAuthToken(resp.data.id);
|
||||
}
|
||||
|
||||
if (resp.data.provider) {
|
||||
this.setProvider(resp.data.provider);
|
||||
}
|
||||
|
||||
if (resp.data.config) {
|
||||
this.setConfig(resp.data.config);
|
||||
}
|
||||
|
||||
if (resp.data.user) {
|
||||
this.setUser(resp.data.user);
|
||||
}
|
||||
|
||||
if (resp.data.data) {
|
||||
this.setData(resp.data.data);
|
||||
}
|
||||
|
@ -179,7 +242,7 @@ export default class Session {
|
|||
}
|
||||
|
||||
this.data = data;
|
||||
this.storage.setItem("data", JSON.stringify(data));
|
||||
this.storage.setItem("sessionData", JSON.stringify(data));
|
||||
|
||||
if (data.user) {
|
||||
this.setUser(data.user);
|
||||
|
@ -264,7 +327,7 @@ export default class Session {
|
|||
|
||||
deleteData() {
|
||||
this.data = null;
|
||||
this.storage.removeItem("data");
|
||||
this.storage.removeItem("sessionData");
|
||||
}
|
||||
|
||||
deleteUser() {
|
||||
|
@ -280,7 +343,7 @@ export default class Session {
|
|||
}
|
||||
|
||||
reset() {
|
||||
this.deleteId();
|
||||
this.deleteAuthentication();
|
||||
this.deleteData();
|
||||
this.deleteUser();
|
||||
this.deleteClipboard();
|
||||
|
@ -289,7 +352,7 @@ export default class Session {
|
|||
sendClientInfo() {
|
||||
const hasConfig = !!window.__CONFIG__;
|
||||
const clientInfo = {
|
||||
session: this.getId(),
|
||||
session: this.getAuthToken(),
|
||||
cssUri: hasConfig ? window.__CONFIG__.cssUri : "",
|
||||
jsUri: hasConfig ? window.__CONFIG__.jsUri : "",
|
||||
version: hasConfig ? window.__CONFIG__.version : "",
|
||||
|
@ -327,16 +390,17 @@ export default class Session {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
// Refresh session information.
|
||||
// Check if the authentication is still valid and update the client session data.
|
||||
if (this.config.isPublic()) {
|
||||
// No authentication in public mode.
|
||||
this.setId(PublicID);
|
||||
// Use a static auth token in public mode, as no additional authentication is required.
|
||||
this.setAuthToken(PublicAuthToken);
|
||||
this.setId(PublicSessionID);
|
||||
return Api.get("session/" + this.getId()).then((resp) => {
|
||||
this.setResp(resp);
|
||||
return Promise.resolve();
|
||||
});
|
||||
} else if (this.hasId()) {
|
||||
// Verify authentication.
|
||||
} else if (this.isAuthenticated()) {
|
||||
// Check the auth token by fetching the client session data from the API.
|
||||
return Api.get("session/" + this.getId())
|
||||
.then((resp) => {
|
||||
this.setResp(resp);
|
||||
|
@ -350,7 +414,7 @@ export default class Session {
|
|||
return Promise.reject();
|
||||
});
|
||||
} else {
|
||||
// No authentication yet.
|
||||
// Skip updating session data if client is not authenticated.
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
@ -367,15 +431,19 @@ export default class Session {
|
|||
}
|
||||
|
||||
onLogout(noRedirect) {
|
||||
// Delete all authentication and session data.
|
||||
this.reset();
|
||||
|
||||
// Perform redirect?
|
||||
if (noRedirect !== true && !this.isLogin()) {
|
||||
window.location = this.config.baseUri + "/";
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logout(noRedirect) {
|
||||
if (this.hasId()) {
|
||||
if (this.isAuthenticated()) {
|
||||
return Api.delete("session/" + this.getId())
|
||||
.then(() => {
|
||||
return this.onLogout(noRedirect);
|
||||
|
|
|
@ -14,19 +14,19 @@ describe("common/session", () => {
|
|||
it("should construct session", () => {
|
||||
const storage = new StorageShim();
|
||||
const session = new Session(storage, config);
|
||||
assert.equal(session.session_id, null);
|
||||
assert.equal(session.authToken, null);
|
||||
});
|
||||
|
||||
it("should set, get and delete token", () => {
|
||||
const storage = new StorageShim();
|
||||
const session = new Session(storage, config);
|
||||
assert.equal(session.hasToken("2lbh9x09"), false);
|
||||
session.setId("999900000000000000000000000000000000000000000000");
|
||||
assert.equal(session.session_id, "999900000000000000000000000000000000000000000000");
|
||||
const result = session.getId();
|
||||
session.setAuthToken("999900000000000000000000000000000000000000000000");
|
||||
assert.equal(session.authToken, "999900000000000000000000000000000000000000000000");
|
||||
const result = session.getAuthToken();
|
||||
assert.equal(result, "999900000000000000000000000000000000000000000000");
|
||||
session.reset();
|
||||
assert.equal(session.session_id, null);
|
||||
assert.equal(session.authToken, null);
|
||||
});
|
||||
|
||||
it("should set, get and delete user", () => {
|
||||
|
@ -48,9 +48,23 @@ describe("common/session", () => {
|
|||
user,
|
||||
};
|
||||
|
||||
assert.equal(session.hasId(), false);
|
||||
assert.equal(session.hasAuthToken(), false);
|
||||
assert.equal(session.isAuthenticated(), false);
|
||||
assert.equal(session.hasProvider(), false);
|
||||
session.setData();
|
||||
assert.equal(session.user.DisplayName, "");
|
||||
session.setData(data);
|
||||
assert.equal(session.hasId(), false);
|
||||
assert.equal(session.hasAuthToken(), false);
|
||||
assert.equal(session.hasProvider(), false);
|
||||
session.setId("a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f");
|
||||
session.setAuthToken("234200000000000000000000000000000000000000000000");
|
||||
session.setProvider("public");
|
||||
assert.equal(session.hasId(), true);
|
||||
assert.equal(session.hasAuthToken(), true);
|
||||
assert.equal(session.isAuthenticated(), true);
|
||||
assert.equal(session.hasProvider(), true);
|
||||
assert.equal(session.user.DisplayName, "Max Example");
|
||||
assert.equal(session.user.SuperAdmin, true);
|
||||
assert.equal(session.user.Role, "admin");
|
||||
|
@ -79,6 +93,11 @@ describe("common/session", () => {
|
|||
it("should get user email", () => {
|
||||
const storage = new StorageShim();
|
||||
const session = new Session(storage, config);
|
||||
|
||||
session.setId("a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f");
|
||||
session.setAuthToken("234200000000000000000000000000000000000000000000");
|
||||
session.setProvider("public");
|
||||
|
||||
const values = {
|
||||
user: {
|
||||
ID: 5,
|
||||
|
@ -88,6 +107,7 @@ describe("common/session", () => {
|
|||
Role: "admin",
|
||||
},
|
||||
};
|
||||
|
||||
session.setData(values);
|
||||
const result = session.getEmail();
|
||||
assert.equal(result, "test@test.com");
|
||||
|
@ -121,6 +141,10 @@ describe("common/session", () => {
|
|||
const result = session.getDisplayName();
|
||||
assert.equal(result, "Max Last");
|
||||
const values2 = {
|
||||
id: "a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f",
|
||||
access_token: "234200000000000000000000000000000000000000000000",
|
||||
provider: "public",
|
||||
data: {},
|
||||
user: {
|
||||
ID: 5,
|
||||
Name: "bar",
|
||||
|
@ -221,18 +245,18 @@ describe("common/session", () => {
|
|||
it("should use session storage", () => {
|
||||
const storage = new StorageShim();
|
||||
const session = new Session(storage, config);
|
||||
assert.equal(storage.getItem("session_storage"), null);
|
||||
assert.equal(storage.getItem("sessionStorage"), null);
|
||||
session.useSessionStorage();
|
||||
assert.equal(storage.getItem("session_storage"), "true");
|
||||
assert.equal(storage.getItem("sessionStorage"), "true");
|
||||
session.deleteData();
|
||||
});
|
||||
|
||||
it("should use local storage", () => {
|
||||
const storage = new StorageShim();
|
||||
const session = new Session(storage, config);
|
||||
assert.equal(storage.getItem("session_storage"), null);
|
||||
assert.equal(storage.getItem("sessionStorage"), null);
|
||||
session.useLocalStorage();
|
||||
assert.equal(storage.getItem("session_storage"), "false");
|
||||
assert.equal(storage.getItem("sessionStorage"), "false");
|
||||
session.deleteData();
|
||||
});
|
||||
|
||||
|
|
|
@ -133,36 +133,46 @@ Mock.onDelete("api/v1/photos/pqbemz8276mhtobh/label/12345").reply(
|
|||
Mock.onPost("api/v1/session").reply(
|
||||
200,
|
||||
{
|
||||
id: "999900000000000000000000000000000000000000000000",
|
||||
id: "5aa770f2a1ef431628d9f17bdf82a0d16865e99d4a1ddd9356e1aabfe6464683",
|
||||
access_token: "999900000000000000000000000000000000000000000000",
|
||||
provider: "test",
|
||||
data: { token: "123token" },
|
||||
user: { ID: 1, UID: "urjysof3b9v7lgex", Name: "test", Email: "test@test.com" },
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
|
||||
Mock.onGet("api/v1/session/234200000000000000000000000000000000000000000000").reply(
|
||||
Mock.onGet("api/v1/session/a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f").reply(
|
||||
200,
|
||||
{
|
||||
id: "234200000000000000000000000000000000000000000000",
|
||||
id: "a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f",
|
||||
access_token: "234200000000000000000000000000000000000000000000",
|
||||
provider: "public",
|
||||
data: { token: "123token" },
|
||||
user: { ID: 1, UID: "urjysof3b9v7lgex", Name: "test", Email: "test@test.com" },
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
|
||||
Mock.onGet("api/v1/session/999900000000000000000000000000000000000000000000").reply(
|
||||
Mock.onGet("api/v1/session/5aa770f2a1ef431628d9f17bdf82a0d16865e99d4a1ddd9356e1aabfe6464683").reply(
|
||||
200,
|
||||
{
|
||||
id: "999900000000000000000000000000000000000000000000",
|
||||
id: "5aa770f2a1ef431628d9f17bdf82a0d16865e99d4a1ddd9356e1aabfe6464683",
|
||||
access_token: "999900000000000000000000000000000000000000000000",
|
||||
provider: "test",
|
||||
data: { token: "123token" },
|
||||
user: { ID: 1, UID: "urjysof3b9v7lgex", Name: "test", Email: "test@test.com" },
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
|
||||
Mock.onDelete("api/v1/session/999900000000000000000000000000000000000000000000").reply(200);
|
||||
Mock.onDelete(
|
||||
"api/v1/session/5aa770f2a1ef431628d9f17bdf82a0d16865e99d4a1ddd9356e1aabfe6464683"
|
||||
).reply(200);
|
||||
|
||||
Mock.onDelete("api/v1/session/234200000000000000000000000000000000000000000000").reply(200);
|
||||
Mock.onDelete(
|
||||
"api/v1/session/a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f"
|
||||
).reply(200);
|
||||
|
||||
Mock.onGet("api/v1/settings").reply(200, { download: true, language: "de" }, mockHeaders);
|
||||
Mock.onPost("api/v1/settings").reply(200, { download: true, language: "en" }, mockHeaders);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Package api provides REST API authentication and request handlers.
|
||||
Package api provides REST-API authentication and request handlers.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ func UpdateClientConfig() {
|
|||
// GET /api/v1/config
|
||||
func GetClientConfig(router *gin.RouterGroup) {
|
||||
router.GET("/config", func(c *gin.Context) {
|
||||
s := Session(SessionID(c))
|
||||
s := Session(AuthToken(c))
|
||||
conf := get.Config()
|
||||
|
||||
if s == nil {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -11,9 +12,9 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// SessionID returns the session ID from the request context,
|
||||
// or an empty string if there is none.
|
||||
func SessionID(c *gin.Context) string {
|
||||
// AuthToken returns the client authentication token from the request context,
|
||||
// or an empty string if none is found.
|
||||
func AuthToken(c *gin.Context) string {
|
||||
// Default is an empty string if no context or ID is set.
|
||||
if c == nil {
|
||||
return ""
|
||||
|
@ -28,9 +29,9 @@ func SessionID(c *gin.Context) string {
|
|||
return BearerToken(c)
|
||||
}
|
||||
|
||||
// BearerToken returns the value of the bearer token header, or an empty string if there is none.
|
||||
// BearerToken returns the client bearer token header value, or an empty string if none is found.
|
||||
func BearerToken(c *gin.Context) string {
|
||||
if authType, bearerToken := Authorization(c); authType == "Bearer" && bearerToken != "" {
|
||||
if authType, bearerToken := Authorization(c); authType == header.BearerAuth && bearerToken != "" {
|
||||
return bearerToken
|
||||
}
|
||||
|
||||
|
@ -40,7 +41,9 @@ func BearerToken(c *gin.Context) string {
|
|||
// Authorization returns the authentication type and token from the authorization request header,
|
||||
// or an empty string if there is none.
|
||||
func Authorization(c *gin.Context) (authType, authToken string) {
|
||||
if s := c.GetHeader(header.Authorization); s == "" {
|
||||
if c == nil {
|
||||
return "", ""
|
||||
} else if s := c.GetHeader(header.Authorization); s == "" {
|
||||
// Ignore.
|
||||
} else if t := strings.Split(s, " "); len(t) != 2 {
|
||||
// Ignore.
|
||||
|
@ -51,6 +54,13 @@ func Authorization(c *gin.Context) (authType, authToken string) {
|
|||
return "", ""
|
||||
}
|
||||
|
||||
// AddRequestAuthorizationHeader adds a bearer token authorization header to a request.
|
||||
func AddRequestAuthorizationHeader(r *http.Request, authToken string) {
|
||||
if authToken != "" {
|
||||
r.Header.Add(header.Authorization, fmt.Sprintf("%s %s", header.BearerAuth, authToken))
|
||||
}
|
||||
}
|
||||
|
||||
// BasicAuth checks the basic authorization header for credentials and returns them if found.
|
||||
//
|
||||
// Note that OAuth 2.0 defines basic authentication differently than RFC 7617, however, this
|
||||
|
@ -59,7 +69,7 @@ func Authorization(c *gin.Context) (authType, authToken string) {
|
|||
func BasicAuth(c *gin.Context) (username, password, cacheKey string) {
|
||||
authType, authToken := Authorization(c)
|
||||
|
||||
if authType != "Basic" || authToken == "" {
|
||||
if authType != header.BasicAuth || authToken == "" {
|
||||
return "", "", ""
|
||||
}
|
||||
|
157
internal/api/api_request_headers_test.go
Normal file
157
internal/api/api_request_headers_test.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/server/header"
|
||||
)
|
||||
|
||||
func TestAuthToken(t *testing.T) {
|
||||
t.Run("None", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// No headers have been set, so no token should be returned.
|
||||
token := AuthToken(c)
|
||||
assert.Equal(t, "", token)
|
||||
})
|
||||
t.Run("BearerToken", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
AddRequestAuthorizationHeader(c.Request, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
authToken := AuthToken(c)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", authToken)
|
||||
bearerToken := BearerToken(c)
|
||||
assert.Equal(t, authToken, bearerToken)
|
||||
})
|
||||
t.Run("X-Session-ID", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
c.Request.Header.Add(header.SessionID, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
authToken := AuthToken(c)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", authToken)
|
||||
bearerToken := BearerToken(c)
|
||||
assert.Equal(t, "", bearerToken)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBearerToken(t *testing.T) {
|
||||
t.Run("None", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// No headers have been set, so no token should be returned.
|
||||
token := BearerToken(c)
|
||||
assert.Equal(t, "", token)
|
||||
})
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
AddRequestAuthorizationHeader(c.Request, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
token := BearerToken(c)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorization(t *testing.T) {
|
||||
t.Run("None", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// No headers have been set, so no token should be returned.
|
||||
authType, authToken := Authorization(c)
|
||||
assert.Equal(t, "", authType)
|
||||
assert.Equal(t, "", authToken)
|
||||
})
|
||||
t.Run("BearerToken", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
c.Request.Header.Add(header.Authorization, "Bearer 69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
authType, authToken := Authorization(c)
|
||||
assert.Equal(t, "Bearer", authType)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", authToken)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
t.Run("None", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// No headers have been set, so no token should be returned.
|
||||
user, pass, key := BasicAuth(c)
|
||||
assert.Equal(t, "", user)
|
||||
assert.Equal(t, "", pass)
|
||||
assert.Equal(t, "", key)
|
||||
})
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
c.Request.Header.Add(header.Authorization, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
|
||||
|
||||
// Check result.
|
||||
user, pass, key := BasicAuth(c)
|
||||
assert.Equal(t, "Aladdin", user)
|
||||
assert.Equal(t, "open sesame", pass)
|
||||
assert.Equal(t, "0cdb723383eb144043424a4a254461658d887396", key)
|
||||
})
|
||||
}
|
|
@ -12,7 +12,9 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/server/header"
|
||||
)
|
||||
|
||||
type CloseableResponseRecorder struct {
|
||||
|
@ -53,7 +55,7 @@ func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config
|
|||
return app, router, get.Config()
|
||||
}
|
||||
|
||||
// Executes an API request with an empty request body.
|
||||
// PerformRequest runs an API request with an empty request body.
|
||||
// See https://medium.com/@craigchilds94/testing-gin-json-responses-1f258ce3b0b1
|
||||
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequest(method, path, nil)
|
||||
|
@ -63,7 +65,7 @@ func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecor
|
|||
return w
|
||||
}
|
||||
|
||||
// Executes an API request with the request body as a string.
|
||||
// PerformRequestWithBody runs an API request with the request body as a string.
|
||||
func PerformRequestWithBody(r http.Handler, method, path, body string) *httptest.ResponseRecorder {
|
||||
reader := strings.NewReader(body)
|
||||
req, _ := http.NewRequest(method, path, reader)
|
||||
|
@ -74,7 +76,7 @@ func PerformRequestWithBody(r http.Handler, method, path, body string) *httptest
|
|||
return w
|
||||
}
|
||||
|
||||
// Executes an API request with a stream response.
|
||||
// PerformRequestWithStream runs an API request with a stream response.
|
||||
func PerformRequestWithStream(r http.Handler, method, path string) *CloseableResponseRecorder {
|
||||
req, _ := http.NewRequest(method, path, nil)
|
||||
w := &CloseableResponseRecorder{httptest.NewRecorder(), make(chan bool, 1)}
|
||||
|
@ -83,3 +85,49 @@ func PerformRequestWithStream(r http.Handler, method, path string) *CloseableRes
|
|||
|
||||
return w
|
||||
}
|
||||
|
||||
// AuthenticateAdmin Register session routes and returns valid SessionId.
|
||||
// Call this func after registering other routes and before performing other requests.
|
||||
func AuthenticateAdmin(app *gin.Engine, router *gin.RouterGroup) (authToken string) {
|
||||
return AuthenticateUser(app, router, "admin", "photoprism")
|
||||
}
|
||||
|
||||
// AuthenticateUser Register session routes and returns valid SessionId.
|
||||
// Call this func after registering other routes and before performing other requests.
|
||||
func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, name string, password string) (authToken string) {
|
||||
CreateSession(router)
|
||||
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", form.AsJson(form.Login{
|
||||
UserName: name,
|
||||
Password: password,
|
||||
}))
|
||||
|
||||
authToken = r.Header().Get(header.SessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Performs authenticated API request with empty request body.
|
||||
func AuthenticatedRequest(r http.Handler, method, path, authToken string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequest(method, path, nil)
|
||||
|
||||
AddRequestAuthorizationHeader(req, authToken)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Performs an authenticated API request containing the request body as a string.
|
||||
func AuthenticatedRequestWithBody(r http.Handler, method, path, body string, authToken string) *httptest.ResponseRecorder {
|
||||
reader := strings.NewReader(body)
|
||||
req, _ := http.NewRequest(method, path, reader)
|
||||
|
||||
AddRequestAuthorizationHeader(req, authToken)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
return w
|
||||
}
|
||||
|
|
|
@ -43,9 +43,9 @@ var wsConnection = websocket.Upgrader{
|
|||
},
|
||||
}
|
||||
|
||||
// clientInfo represents information provided by the WebSocket client.
|
||||
type clientInfo struct {
|
||||
SessionID string `json:"session"`
|
||||
// wsClient represents information about the WebSocket client.
|
||||
type wsClient struct {
|
||||
AuthToken string `json:"session"`
|
||||
CssUri string `json:"css"`
|
||||
JsUri string `json:"js"`
|
||||
Version string `json:"version"`
|
||||
|
@ -111,18 +111,18 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
|
|||
ws.SetPongHandler(func(string) error { _ = ws.SetReadDeadline(time.Now().Add(wsTimeout)); return nil })
|
||||
|
||||
for {
|
||||
_, m, err := ws.ReadMessage()
|
||||
_, m, readErr := ws.ReadMessage()
|
||||
|
||||
if err != nil {
|
||||
if readErr != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var info clientInfo
|
||||
var info wsClient
|
||||
|
||||
if err := json.Unmarshal(m, &info); err != nil {
|
||||
if jsonErr := json.Unmarshal(m, &info); jsonErr != nil {
|
||||
// Do nothing.
|
||||
} else {
|
||||
if s := Session(info.SessionID); s != nil {
|
||||
if s := Session(info.AuthToken); s != nil {
|
||||
wsAuth.mutex.Lock()
|
||||
wsAuth.sid[connId] = s.ID
|
||||
wsAuth.rid[connId] = s.RefID
|
||||
|
|
|
@ -20,7 +20,7 @@ const (
|
|||
// PublishPhotoEvent publishes updated photo data after changes have been made.
|
||||
func PublishPhotoEvent(ev EntityEvent, uid string, c *gin.Context) {
|
||||
if result, _, err := search.Photos(form.SearchPhotos{UID: uid, Merged: true}); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s photo %s", "%s"}, SessionID(c), string(ev), uid, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s photo %s", "%s"}, AuthToken(c), string(ev), uid, err)
|
||||
} else {
|
||||
event.PublishEntities("photos", string(ev), result)
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ func PublishPhotoEvent(ev EntityEvent, uid string, c *gin.Context) {
|
|||
func PublishAlbumEvent(ev EntityEvent, uid string, c *gin.Context) {
|
||||
f := form.SearchAlbums{UID: uid}
|
||||
if result, err := search.Albums(f); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s album %s", "%s"}, SessionID(c), string(ev), uid, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s album %s", "%s"}, AuthToken(c), string(ev), uid, err)
|
||||
} else {
|
||||
event.PublishEntities("albums", string(ev), result)
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ func PublishAlbumEvent(ev EntityEvent, uid string, c *gin.Context) {
|
|||
func PublishLabelEvent(ev EntityEvent, uid string, c *gin.Context) {
|
||||
f := form.SearchLabels{UID: uid}
|
||||
if result, err := search.Labels(f); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s label %s", "%s"}, SessionID(c), string(ev), uid, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s label %s", "%s"}, AuthToken(c), string(ev), uid, err)
|
||||
} else {
|
||||
event.PublishEntities("labels", string(ev), result)
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ func PublishLabelEvent(ev EntityEvent, uid string, c *gin.Context) {
|
|||
func PublishSubjectEvent(ev EntityEvent, uid string, c *gin.Context) {
|
||||
f := form.SearchSubjects{UID: uid}
|
||||
if result, err := search.Subjects(f); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s subject %s", "%s"}, SessionID(c), string(ev), uid, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s subject %s", "%s"}, AuthToken(c), string(ev), uid, err)
|
||||
} else {
|
||||
event.PublishEntities("subjects", string(ev), result)
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/server/header"
|
||||
)
|
||||
|
||||
// AuthenticateAdmin Register session routes and returns valid SessionId.
|
||||
// Call this func after registering other routes and before performing other requests.
|
||||
func AuthenticateAdmin(app *gin.Engine, router *gin.RouterGroup) (sessId string) {
|
||||
return AuthenticateUser(app, router, "admin", "photoprism")
|
||||
}
|
||||
|
||||
// AuthenticateUser Register session routes and returns valid SessionId.
|
||||
// Call this func after registering other routes and before performing other requests.
|
||||
func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, name string, password string) (sessId string) {
|
||||
CreateSession(router)
|
||||
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", form.AsJson(form.Login{
|
||||
UserName: name,
|
||||
Password: password,
|
||||
}))
|
||||
|
||||
sessId = r.Header().Get(header.SessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Performs authenticated API request with empty request body.
|
||||
func AuthenticatedRequest(r http.Handler, method, path, sess string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequest(method, path, nil)
|
||||
|
||||
if sess != "" {
|
||||
req.Header.Add(header.SessionID, sess)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Performs an authenticated API request containing the request body as a string.
|
||||
func AuthenticatedRequestWithBody(r http.Handler, method, path, body string, sess string) *httptest.ResponseRecorder {
|
||||
reader := strings.NewReader(body)
|
||||
req, _ := http.NewRequest(method, path, reader)
|
||||
|
||||
if sess != "" {
|
||||
req.Header.Add(header.SessionID, sess)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
return w
|
||||
}
|
|
@ -1,25 +1,65 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// Session finds the client session for the given ID or returns nil otherwise.
|
||||
func Session(id string) *entity.Session {
|
||||
// Skip authentication if app is running in public mode.
|
||||
// Session finds the client session for the specified
|
||||
// auth token, or returns nil if not found.
|
||||
func Session(authToken string) *entity.Session {
|
||||
// Skip authentication when running in public mode.
|
||||
if get.Config().Public() {
|
||||
return get.Session().Public()
|
||||
} else if id == "" {
|
||||
} else if !rnd.IsAuthToken(authToken) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find session or otherwise return nil.
|
||||
s, err := get.Session().Get(id)
|
||||
|
||||
if err != nil {
|
||||
// Find the session based on the hashed auth
|
||||
// token used as id, or return nil otherwise.
|
||||
if s, err := get.Session().Get(rnd.SessionID(authToken)); err != nil {
|
||||
return nil
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// SessionResponse returns authentication response data based on the session and client config.
|
||||
func SessionResponse(authToken string, sess *entity.Session, conf config.ClientConfig) gin.H {
|
||||
if authToken == "" {
|
||||
return gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"expires_in": sess.ExpiresIn(),
|
||||
"provider": sess.Provider().String(),
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": conf,
|
||||
}
|
||||
} else {
|
||||
return gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"access_token": authToken,
|
||||
"token_type": sess.AuthTokenType(),
|
||||
"expires_in": sess.ExpiresIn(),
|
||||
"provider": sess.Provider().String(),
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": conf,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SessionDeleteResponse returns a confirmation response for deleted sessions.
|
||||
func SessionDeleteResponse(authToken string) gin.H {
|
||||
if authToken == "" {
|
||||
return gin.H{"status": "ok"}
|
||||
} else {
|
||||
return gin.H{"status": "ok", "id": authToken, "access_token": authToken}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ func Auth(c *gin.Context, resource acl.Resource, grant acl.Permission) *entity.S
|
|||
func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *entity.Session) {
|
||||
// Get the client IP and session ID from the request headers.
|
||||
ip := ClientIP(c)
|
||||
sid := SessionID(c)
|
||||
authToken := AuthToken(c)
|
||||
|
||||
// Find active session to perform authorization check or deny if no session was found.
|
||||
if s = Session(sid); s == nil {
|
||||
if s = Session(authToken); s == nil {
|
||||
event.AuditWarn([]string{ip, "unauthenticated", "%s %s", "denied"}, grants.String(), string(resource))
|
||||
return entity.SessionStatusUnauthorized()
|
||||
} else {
|
89
internal/api/session_acl_test.go
Normal file
89
internal/api/session_acl_test.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/session"
|
||||
)
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
AddRequestAuthorizationHeader(c.Request, session.PublicAuthToken)
|
||||
|
||||
// Check auth token.
|
||||
authToken := AuthToken(c)
|
||||
assert.Equal(t, session.PublicAuthToken, authToken)
|
||||
|
||||
// Check successful authorization in public mode.
|
||||
s := Auth(c, acl.ResourceFiles, acl.ActionUpdate)
|
||||
assert.NotNil(t, s)
|
||||
assert.Equal(t, "admin", s.Username())
|
||||
assert.Equal(t, session.PublicID, s.ID)
|
||||
assert.Equal(t, http.StatusOK, s.HttpStatus())
|
||||
assert.False(t, s.Abort(c))
|
||||
|
||||
// Check failed authorization in public mode.
|
||||
s = Auth(c, acl.ResourceUsers, acl.ActionUpload)
|
||||
assert.NotNil(t, s)
|
||||
assert.Equal(t, "", s.Username())
|
||||
assert.Equal(t, "", s.ID)
|
||||
assert.Equal(t, http.StatusForbidden, s.HttpStatus())
|
||||
assert.True(t, s.Abort(c))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthAny(t *testing.T) {
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
AddRequestAuthorizationHeader(c.Request, session.PublicAuthToken)
|
||||
|
||||
// Check auth token.
|
||||
authToken := AuthToken(c)
|
||||
assert.Equal(t, session.PublicAuthToken, authToken)
|
||||
|
||||
// Check successful authorization in public mode.
|
||||
s := AuthAny(c, acl.ResourceFiles, acl.Permissions{acl.ActionUpdate})
|
||||
assert.NotNil(t, s)
|
||||
assert.Equal(t, "admin", s.Username())
|
||||
assert.Equal(t, session.PublicID, s.ID)
|
||||
assert.Equal(t, http.StatusOK, s.HttpStatus())
|
||||
assert.False(t, s.Abort(c))
|
||||
|
||||
// Check failed authorization in public mode.
|
||||
s = AuthAny(c, acl.ResourceUsers, acl.Permissions{acl.ActionUpload})
|
||||
assert.NotNil(t, s)
|
||||
assert.Equal(t, "", s.Username())
|
||||
assert.Equal(t, "", s.ID)
|
||||
assert.Equal(t, http.StatusForbidden, s.HttpStatus())
|
||||
assert.True(t, s.Abort(c))
|
||||
|
||||
// Check successful authorization with multiple actions in public mode.
|
||||
s = AuthAny(c, acl.ResourceUsers, acl.Permissions{acl.ActionUpload, acl.ActionView})
|
||||
assert.NotNil(t, s)
|
||||
assert.Equal(t, "admin", s.Username())
|
||||
assert.Equal(t, session.PublicID, s.ID)
|
||||
assert.Equal(t, http.StatusOK, s.HttpStatus())
|
||||
assert.False(t, s.Abort(c))
|
||||
})
|
||||
}
|
|
@ -31,15 +31,12 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
// Skip authentication if app is running in public mode.
|
||||
if conf.Public() {
|
||||
sess := get.Session().Public()
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"provider": sess.AuthProvider,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": conf.ClientPublic(),
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
|
||||
// Response includes admin account data, session data, and client config values.
|
||||
response := SessionResponse(sess.AuthToken(), sess, conf.ClientPublic())
|
||||
|
||||
// Return JSON response.
|
||||
c.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -53,7 +50,7 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
var isNew bool
|
||||
|
||||
// Find existing session, if any.
|
||||
if s := Session(SessionID(c)); s != nil {
|
||||
if s := Session(AuthToken(c)); s != nil {
|
||||
// Update existing session.
|
||||
sess = s
|
||||
} else {
|
||||
|
@ -80,22 +77,12 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Add session id to response headers.
|
||||
AddSessionHeader(c, sess.ID)
|
||||
AddSessionHeader(c, sess.AuthToken())
|
||||
|
||||
// Get config values for the UI.
|
||||
clientConfig := conf.ClientSession(sess)
|
||||
// Response includes user data, session data, and client config values.
|
||||
response := SessionResponse(sess.AuthToken(), sess, conf.ClientSession(sess))
|
||||
|
||||
// User information, session data, and client config values.
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"provider": sess.AuthProvider,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": clientConfig,
|
||||
}
|
||||
|
||||
// Send JSON response.
|
||||
c.JSON(sess.HttpStatus(), data)
|
||||
// Return JSON response.
|
||||
c.JSON(sess.HttpStatus(), response)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/session"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
@ -20,38 +21,54 @@ func DeleteSession(router *gin.RouterGroup) {
|
|||
router.DELETE("/session/:id", func(c *gin.Context) {
|
||||
id := clean.ID(c.Param("id"))
|
||||
|
||||
// Abort if ID is missing.
|
||||
// Abort if authentication token is missing or empty.
|
||||
if id == "" {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
} else if get.Config().Public() {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "authentication disabled", "id": id})
|
||||
c.JSON(http.StatusOK, gin.H{"status": "running in public mode", "id": session.PublicAuthToken})
|
||||
return
|
||||
}
|
||||
|
||||
// Find session by reference ID.
|
||||
if !rnd.IsRefID(id) {
|
||||
// Do nothing.
|
||||
} else if s := Session(SessionID(c)); s == nil {
|
||||
entity.SessionStatusUnauthorized().Abort(c)
|
||||
return
|
||||
} else if !acl.Resources.AllowAll(acl.ResourceUsers, s.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
s.Abort(c)
|
||||
return
|
||||
} else if ref := entity.FindSessionByRefID(id); ref == nil {
|
||||
AbortNotFound(c)
|
||||
return
|
||||
// Only admins may delete other sessions by reference id.
|
||||
if rnd.IsRefID(id) {
|
||||
if s := Session(AuthToken(c)); s == nil {
|
||||
entity.SessionStatusUnauthorized().Abort(c)
|
||||
return
|
||||
} else if s.Abort(c) {
|
||||
return
|
||||
} else if !acl.Resources.AllowAll(acl.ResourceUsers, s.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
s.Abort(c)
|
||||
return
|
||||
} else if ref := entity.FindSessionByRefID(id); ref == nil {
|
||||
AbortNotFound(c)
|
||||
return
|
||||
} else {
|
||||
id = ref.ID
|
||||
}
|
||||
} else {
|
||||
id = ref.ID
|
||||
if s := Session(AuthToken(c)); s == nil {
|
||||
entity.SessionStatusUnauthorized().Abort(c)
|
||||
return
|
||||
} else if s.Abort(c) {
|
||||
return
|
||||
} else if s.ID != id {
|
||||
entity.SessionStatusForbidden().Abort(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Delete session by ID.
|
||||
// Delete session.
|
||||
if err := get.Session().Delete(id); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s"}, err)
|
||||
} else {
|
||||
event.AuditDebug([]string{ClientIP(c), "session deleted"})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id})
|
||||
// Response includes the auth token for confirmation.
|
||||
response := SessionDeleteResponse(id)
|
||||
|
||||
// Return JSON response.
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -17,22 +17,24 @@ func GetSession(router *gin.RouterGroup) {
|
|||
router.GET("/session/:id", func(c *gin.Context) {
|
||||
id := clean.ID(c.Param("id"))
|
||||
|
||||
// Check authentication token.
|
||||
if id == "" {
|
||||
// Abort if authentication token is missing or empty.
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
} else if id != SessionID(c) {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
conf := get.Config()
|
||||
authToken := AuthToken(c)
|
||||
|
||||
// Skip authentication if app is running in public mode.
|
||||
var sess *entity.Session
|
||||
if conf.Public() {
|
||||
sess = get.Session().Public()
|
||||
id = sess.ID
|
||||
authToken = sess.AuthToken()
|
||||
} else {
|
||||
sess = Session(id)
|
||||
sess = Session(authToken)
|
||||
}
|
||||
|
||||
switch {
|
||||
|
@ -42,7 +44,7 @@ func GetSession(router *gin.RouterGroup) {
|
|||
case sess.Expired(), sess.ID == "":
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
case sess.Invalid():
|
||||
case sess.Invalid(), sess.ID != id && !conf.Public():
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
@ -51,18 +53,12 @@ func GetSession(router *gin.RouterGroup) {
|
|||
sess.RefreshUser()
|
||||
|
||||
// Add session id to response headers.
|
||||
AddSessionHeader(c, sess.ID)
|
||||
AddSessionHeader(c, authToken)
|
||||
|
||||
// Send JSON response with user information, session data, and client config values.
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"provider": sess.AuthProvider,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": get.Config().ClientSession(sess),
|
||||
}
|
||||
// Response includes user data, session data, and client config values.
|
||||
response := SessionResponse(authToken, sess, get.Config().ClientSession(sess))
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
// Return JSON response.
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
)
|
||||
|
||||
// CreateOauthToken creates a new access token and returns it as JSON
|
||||
// if the client's credentials have been successfully validated.
|
||||
// CreateOauthToken creates a new access token for clients that
|
||||
// authenticate with valid OAuth2 client credentials.
|
||||
//
|
||||
// POST /api/v1/oauth/token
|
||||
func CreateOauthToken(router *gin.RouterGroup) {
|
||||
|
@ -60,7 +60,7 @@ func CreateOauthToken(router *gin.RouterGroup) {
|
|||
limiter.Login.Reserve(clientIP)
|
||||
return
|
||||
} else if !client.AuthEnabled {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create access token", "authentication disabled"}, f.ClientID)
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create access token", "running in public mode"}, f.ClientID)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if client.AuthMethod != authn.MethodOAuth2.String() {
|
||||
|
@ -92,13 +92,14 @@ func CreateOauthToken(router *gin.RouterGroup) {
|
|||
event.AuditInfo([]string{clientIP, "client %s", "session %s", "access token created"}, f.ClientID, sess.RefID)
|
||||
}
|
||||
|
||||
// Return access token.
|
||||
// Response includes access token, token type, and token lifetime.
|
||||
data := gin.H{
|
||||
"access_token": sess.ID,
|
||||
"token_type": "Bearer",
|
||||
"access_token": sess.AuthToken(),
|
||||
"token_type": sess.AuthTokenType(),
|
||||
"expires_in": sess.ExpiresIn(),
|
||||
}
|
||||
|
||||
// Return JSON response.
|
||||
c.JSON(http.StatusOK, data)
|
||||
})
|
||||
}
|
|
@ -9,21 +9,55 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/session"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestSessionID(t *testing.T) {
|
||||
t.Run("NoContext", func(t *testing.T) {
|
||||
result := SessionID(nil)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
assert.Equal(t, session.Public, Session(""))
|
||||
assert.Equal(t, session.Public, Session("638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"))
|
||||
sess := get.Session().Public()
|
||||
assert.Equal(t, sess, Session(""))
|
||||
assert.Equal(t, sess, Session("638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSessionResponse(t *testing.T) {
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
sess := get.Session().Public()
|
||||
conf := get.Config().ClientSession(sess)
|
||||
|
||||
// Create response in public mode.
|
||||
result := SessionResponse(sess.AuthToken(), sess, conf)
|
||||
|
||||
// Check response.
|
||||
assert.Equal(t, "ok", result["status"])
|
||||
assert.Equal(t, sess.ID, result["id"])
|
||||
assert.Equal(t, sess.AuthToken(), result["access_token"])
|
||||
assert.Equal(t, sess.AuthTokenType(), result["token_type"])
|
||||
assert.Equal(t, sess.ExpiresIn(), result["expires_in"])
|
||||
assert.Equal(t, sess.Provider().String(), result["provider"])
|
||||
assert.Equal(t, sess.User(), result["user"])
|
||||
assert.Equal(t, sess.Data(), result["data"])
|
||||
assert.Equal(t, conf, result["config"])
|
||||
})
|
||||
t.Run("NoAuthToken", func(t *testing.T) {
|
||||
sess := get.Session().Public()
|
||||
conf := get.Config().ClientSession(sess)
|
||||
|
||||
// Create response without auth token.
|
||||
result := SessionResponse("", sess, conf)
|
||||
|
||||
// Check response.
|
||||
assert.Equal(t, "ok", result["status"])
|
||||
assert.Equal(t, sess.ID, result["id"])
|
||||
assert.Nil(t, result["access_token"])
|
||||
assert.Nil(t, result["token_type"])
|
||||
assert.Equal(t, sess.ExpiresIn(), result["expires_in"])
|
||||
assert.Equal(t, sess.Provider().String(), result["provider"])
|
||||
assert.Equal(t, sess.User(), result["user"])
|
||||
assert.Equal(t, sess.Data(), result["data"])
|
||||
assert.Equal(t, conf, result["config"])
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -34,6 +68,7 @@ func TestCreateSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateSession(router)
|
||||
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "admin", "password": "photoprism"}`)
|
||||
log.Debugf("BODY: %s", r.Body.String())
|
||||
val2 := gjson.Get(r.Body.String(), "user.Name")
|
||||
|
@ -46,6 +81,7 @@ func TestCreateSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateSession(router)
|
||||
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": 123, "password": "xxx"}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
|
@ -55,6 +91,7 @@ func TestCreateSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateSession(router)
|
||||
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "admin", "password": "photoprism", "token": "xxx"}`)
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
|
@ -63,9 +100,9 @@ func TestCreateSession(t *testing.T) {
|
|||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
// CreateSession(router)
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
r := AuthenticatedRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"token": "xxx"}`, sessId)
|
||||
authToken := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
r := AuthenticatedRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"token": "xxx"}`, authToken)
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
t.Run("VisitorInvalidToken", func(t *testing.T) {
|
||||
|
@ -74,6 +111,7 @@ func TestCreateSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateSession(router)
|
||||
|
||||
r := AuthenticatedRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"token": "xxx"}`, "345346")
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
|
@ -82,13 +120,16 @@ func TestCreateSession(t *testing.T) {
|
|||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
r := AuthenticatedRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"token": "1jxf3jfn2k"}`, sessId)
|
||||
authToken := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
r := AuthenticatedRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"token": "1jxf3jfn2k"}`, authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("PublicValidToken", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
|
||||
CreateSession(router)
|
||||
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "admin", "password": "photoprism", "token": "1jxf3jfn2k"}`)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
@ -128,6 +169,7 @@ func TestCreateSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateSession(router)
|
||||
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "bob", "password": "Bobbob123!"}`)
|
||||
userEmail := gjson.Get(r.Body.String(), "user.Email")
|
||||
userName := gjson.Get(r.Body.String(), "user.Name")
|
||||
|
@ -141,6 +183,7 @@ func TestCreateSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
CreateSession(router)
|
||||
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "bob", "password": "helloworld"}`)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, i18n.Msg(i18n.ErrInvalidCredentials), val.String())
|
||||
|
@ -155,10 +198,10 @@ func TestGetSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
GetSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
sessId := AuthenticateAdmin(app, router)
|
||||
r := PerformRequest(app, http.MethodGet, "/api/v1/session/"+sessId)
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
r := PerformRequest(app, http.MethodGet, "/api/v1/session/"+rnd.SessionID(authToken))
|
||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||
})
|
||||
t.Run("AdminAuthenticatedRequest", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
|
@ -166,9 +209,10 @@ func TestGetSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
GetSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
sessId := AuthenticateAdmin(app, router)
|
||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/session/"+sessId, sessId)
|
||||
t.Logf("Session ID: %s", authToken)
|
||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
}
|
||||
|
@ -180,10 +224,12 @@ func TestDeleteSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
DeleteSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
sessId := AuthenticateAdmin(app, router)
|
||||
// f9ae12e95a01bcc7faae6497124cd721eaf13c1dad301dbc
|
||||
t.Logf("authToken: %s", authToken)
|
||||
|
||||
r := PerformRequest(app, http.MethodDelete, "/api/v1/session/"+sessId)
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("AdminAuthenticatedRequest", func(t *testing.T) {
|
||||
|
@ -192,10 +238,9 @@ func TestDeleteSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
DeleteSession(router)
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
sessId := AuthenticateAdmin(app, router)
|
||||
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+sessId, sessId)
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("UserWithoutAuthentication", func(t *testing.T) {
|
||||
|
@ -204,11 +249,10 @@ func TestDeleteSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
DeleteSession(router)
|
||||
bobToken := "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"
|
||||
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
r := PerformRequest(app, http.MethodDelete, "/api/v1/session/"+sessId)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
r := PerformRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(bobToken))
|
||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||
})
|
||||
t.Run("UserAuthenticatedRequest", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
|
@ -216,22 +260,45 @@ func TestDeleteSession(t *testing.T) {
|
|||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
DeleteSession(router)
|
||||
authToken := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+sessId, sessId)
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(authToken), authToken)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("AliceSessionAsBob", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
DeleteSession(router)
|
||||
bobToken := AuthenticateUser(app, router, "bob", "Bobbob123!")
|
||||
aliceToken := "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"
|
||||
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(aliceToken), bobToken)
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
t.Run("BobSessionAsAlice", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
DeleteSession(router)
|
||||
aliceToken := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
bobToken := "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"
|
||||
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(bobToken), aliceToken)
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
t.Run("InvalidSession", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
sessId := "638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"
|
||||
|
||||
DeleteSession(router)
|
||||
authToken := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
deleteToken := "638bffc9b86a8fda0d908ebee84a43930cb8d1e3507f4aa0"
|
||||
|
||||
r := PerformRequest(app, http.MethodDelete, "/api/v1/session/"+sessId)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
r := AuthenticatedRequest(app, http.MethodDelete, "/api/v1/session/"+rnd.SessionID(deleteToken), authToken)
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ func authAddAction(ctx *cli.Context) error {
|
|||
fmt.Printf("\nPLEASE WRITE DOWN THE FOLLOWING RANDOMLY GENERATED PERSONAL ACCESS TOKEN, AS YOU WILL NOT BE ABLE TO SEE IT AGAIN:\n")
|
||||
}
|
||||
|
||||
result := report.Credentials("Access Token", sess.ID, "Authorization Scope", sess.Scope())
|
||||
result := report.Credentials("Access Token", sess.AuthToken(), "Authorization Scope", sess.Scope())
|
||||
|
||||
fmt.Printf("\n%s\n", result)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/server/header"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/list"
|
||||
|
@ -35,6 +36,7 @@ type Session struct {
|
|||
UserUID string `gorm:"type:VARBINARY(42);index;default:'';" json:"UserUID" yaml:"UserUID,omitempty"`
|
||||
UserName string `gorm:"size:64;index;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
user *User `gorm:"-"`
|
||||
authToken string `gorm:"-"`
|
||||
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"`
|
||||
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"`
|
||||
AuthDomain string `gorm:"type:VARBINARY(255);default:'';" json:"AuthDomain" yaml:"AuthDomain,omitempty"`
|
||||
|
@ -66,17 +68,12 @@ func (Session) TableName() string {
|
|||
|
||||
// NewSession creates a new session using the maxAge and timeout in seconds.
|
||||
func NewSession(lifetime, timeout int64) (m *Session) {
|
||||
created := TimeStamp()
|
||||
m = &Session{}
|
||||
|
||||
m = &Session{
|
||||
ID: rnd.SessionID(),
|
||||
RefID: rnd.RefID(SessionPrefix),
|
||||
CreatedAt: created,
|
||||
UpdatedAt: created,
|
||||
}
|
||||
m.Regenerate()
|
||||
|
||||
if lifetime > 0 {
|
||||
m.SessExpires = created.Unix() + lifetime
|
||||
m.SessExpires = TimeStamp().Unix() + lifetime
|
||||
}
|
||||
|
||||
if timeout > 0 {
|
||||
|
@ -142,9 +139,27 @@ func FindSessionByRefID(refId string) *Session {
|
|||
return m
|
||||
}
|
||||
|
||||
// RegenerateID regenerated the random session ID.
|
||||
func (m *Session) RegenerateID() *Session {
|
||||
if m.ID == "" {
|
||||
// AuthToken returns the secret client authentication token.
|
||||
func (m *Session) AuthToken() string {
|
||||
return m.authToken
|
||||
}
|
||||
|
||||
// SetAuthToken sets a custom authentication token.
|
||||
func (m *Session) SetAuthToken(authToken string) *Session {
|
||||
m.authToken = authToken
|
||||
m.ID = rnd.SessionID(authToken)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// AuthTokenType returns the authentication token type.
|
||||
func (m *Session) AuthTokenType() string {
|
||||
return header.BearerAuth
|
||||
}
|
||||
|
||||
// Regenerate (re-)initializes the session with a random auth token, ID, and RefID.
|
||||
func (m *Session) Regenerate() *Session {
|
||||
if !rnd.IsSessionID(m.ID) {
|
||||
// Do not delete the old session if no ID is set yet.
|
||||
} else if err := m.Delete(); err != nil {
|
||||
event.AuditErr([]string{m.IP(), "session %s", "failed to delete", "%s"}, m.RefID, err)
|
||||
|
@ -154,7 +169,7 @@ func (m *Session) RegenerateID() *Session {
|
|||
|
||||
generated := TimeStamp()
|
||||
|
||||
m.ID = rnd.SessionID()
|
||||
m.SetAuthToken(rnd.AuthToken())
|
||||
m.RefID = rnd.RefID(SessionPrefix)
|
||||
m.CreatedAt = generated
|
||||
m.UpdatedAt = generated
|
||||
|
@ -222,7 +237,7 @@ func (m *Session) BeforeCreate(scope *gorm.Scope) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
m.ID = rnd.SessionID()
|
||||
m.Regenerate()
|
||||
|
||||
return scope.SetColumn("ID", m.ID)
|
||||
}
|
||||
|
|
|
@ -15,13 +15,17 @@ import (
|
|||
var sessionCacheExpiration = 15 * time.Minute
|
||||
var sessionCache = gc.New(sessionCacheExpiration, 5*time.Minute)
|
||||
|
||||
// FindSession returns an existing session or nil if not found.
|
||||
// FindSessionByAuthToken finds a session based on the auth token string or returns nil if it does not exist.
|
||||
func FindSessionByAuthToken(token string) (*Session, error) {
|
||||
return FindSession(rnd.SessionID(token))
|
||||
}
|
||||
|
||||
// FindSession finds a session based on the id string or returns nil if it does not exist.
|
||||
func FindSession(id string) (*Session, error) {
|
||||
found := &Session{}
|
||||
|
||||
// Valid id?
|
||||
if !rnd.IsSessionID(id) {
|
||||
return found, fmt.Errorf("id %s is invalid", clean.LogQuote(id))
|
||||
return found, fmt.Errorf("invalid session id")
|
||||
}
|
||||
|
||||
// Find the session in the cache with a fallback to the database.
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestFlushSessionCache(t *testing.T) {
|
||||
|
@ -15,6 +15,72 @@ func TestFlushSessionCache(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestFindSessionByAuthToken(t *testing.T) {
|
||||
t.Run("EmptyID", func(t *testing.T) {
|
||||
if _, err := FindSessionByAuthToken(""); err == nil {
|
||||
t.Fatal("error expected")
|
||||
}
|
||||
})
|
||||
t.Run("InvalidID", func(t *testing.T) {
|
||||
if _, err := FindSessionByAuthToken("as6sg6bxpogaaba7"); err == nil {
|
||||
t.Fatal("error expected")
|
||||
}
|
||||
})
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
if _, err := FindSessionByAuthToken(rnd.AuthToken()); err == nil {
|
||||
t.Fatal("error expected")
|
||||
}
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
if result, err := FindSessionByAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"), result.ID)
|
||||
assert.Equal(t, UserFixtures.Pointer("alice").UserUID, result.UserUID)
|
||||
assert.Equal(t, UserFixtures.Pointer("alice").UserName, result.UserName)
|
||||
}
|
||||
if cached, err := FindSessionByAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"), cached.ID)
|
||||
assert.Equal(t, UserFixtures.Pointer("alice").UserUID, cached.UserUID)
|
||||
assert.Equal(t, UserFixtures.Pointer("alice").UserName, cached.UserName)
|
||||
}
|
||||
})
|
||||
t.Run("Bob", func(t *testing.T) {
|
||||
if result, err := FindSessionByAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"), result.ID)
|
||||
assert.Equal(t, UserFixtures.Pointer("bob").UserUID, result.UserUID)
|
||||
assert.Equal(t, UserFixtures.Pointer("bob").UserName, result.UserName)
|
||||
}
|
||||
if cached, err := FindSessionByAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"), cached.ID)
|
||||
assert.Equal(t, UserFixtures.Pointer("bob").UserUID, cached.UserUID)
|
||||
assert.Equal(t, UserFixtures.Pointer("bob").UserName, cached.UserName)
|
||||
}
|
||||
})
|
||||
t.Run("Visitor", func(t *testing.T) {
|
||||
if result, err := FindSessionByAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"), result.ID)
|
||||
assert.Equal(t, Visitor.UserUID, result.UserUID)
|
||||
assert.Equal(t, Visitor.UserName, result.UserName)
|
||||
}
|
||||
if cached, err := FindSessionByAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"), cached.ID)
|
||||
assert.Equal(t, Visitor.UserUID, cached.UserUID)
|
||||
assert.Equal(t, Visitor.UserName, cached.UserName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindSession(t *testing.T) {
|
||||
t.Run("EmptyID", func(t *testing.T) {
|
||||
if _, err := FindSession(""); err == nil {
|
||||
|
@ -27,54 +93,54 @@ func TestFindSession(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
if _, err := FindSession(rnd.SessionID()); err == nil {
|
||||
if _, err := FindSession(rnd.AuthToken()); err == nil {
|
||||
t.Fatal("error expected")
|
||||
}
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
if result, err := FindSession("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"); err != nil {
|
||||
if result, err := FindSession(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", result.ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"), result.ID)
|
||||
assert.Equal(t, UserFixtures.Pointer("alice").UserUID, result.UserUID)
|
||||
assert.Equal(t, UserFixtures.Pointer("alice").UserName, result.UserName)
|
||||
}
|
||||
if cached, err := FindSession("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"); err != nil {
|
||||
if cached, err := FindSession(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", cached.ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"), cached.ID)
|
||||
assert.Equal(t, UserFixtures.Pointer("alice").UserUID, cached.UserUID)
|
||||
assert.Equal(t, UserFixtures.Pointer("alice").UserName, cached.UserName)
|
||||
}
|
||||
})
|
||||
t.Run("Bob", func(t *testing.T) {
|
||||
if result, err := FindSession("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"); err != nil {
|
||||
if result, err := FindSession(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1", result.ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"), result.ID)
|
||||
assert.Equal(t, UserFixtures.Pointer("bob").UserUID, result.UserUID)
|
||||
assert.Equal(t, UserFixtures.Pointer("bob").UserName, result.UserName)
|
||||
}
|
||||
if cached, err := FindSession("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"); err != nil {
|
||||
if cached, err := FindSession(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1", cached.ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"), cached.ID)
|
||||
assert.Equal(t, UserFixtures.Pointer("bob").UserUID, cached.UserUID)
|
||||
assert.Equal(t, UserFixtures.Pointer("bob").UserName, cached.UserName)
|
||||
}
|
||||
})
|
||||
t.Run("Visitor", func(t *testing.T) {
|
||||
if result, err := FindSession("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"); err != nil {
|
||||
if result, err := FindSession(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3", result.ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"), result.ID)
|
||||
assert.Equal(t, Visitor.UserUID, result.UserUID)
|
||||
assert.Equal(t, Visitor.UserName, result.UserName)
|
||||
}
|
||||
if cached, err := FindSession("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"); err != nil {
|
||||
if cached, err := FindSession(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3", cached.ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"), cached.ID)
|
||||
assert.Equal(t, Visitor.UserUID, cached.UserUID)
|
||||
assert.Equal(t, Visitor.UserName, cached.UserName)
|
||||
}
|
||||
|
@ -84,26 +150,26 @@ func TestFindSession(t *testing.T) {
|
|||
func TestCacheSession(t *testing.T) {
|
||||
t.Run("bob", func(t *testing.T) {
|
||||
sessionCache.Flush()
|
||||
r, b := sessionCache.Get("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
r, b := sessionCache.Get(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"))
|
||||
assert.Empty(t, r)
|
||||
assert.False(t, b)
|
||||
bob := FindSessionByRefID("sessxkkcabce")
|
||||
CacheSession(bob, time.Hour)
|
||||
r2, b2 := sessionCache.Get("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
r2, b2 := sessionCache.Get(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"))
|
||||
assert.NotEmpty(t, r2)
|
||||
assert.True(t, b2)
|
||||
sessionCache.Flush()
|
||||
r3, b3 := sessionCache.Get("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
r3, b3 := sessionCache.Get(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"))
|
||||
assert.Empty(t, r3)
|
||||
assert.False(t, b3)
|
||||
})
|
||||
t.Run("duration 0", func(t *testing.T) {
|
||||
r, b := sessionCache.Get("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
r, b := sessionCache.Get(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"))
|
||||
assert.Empty(t, r)
|
||||
assert.False(t, b)
|
||||
alice := FindSessionByRefID("sessxkkcabcd")
|
||||
CacheSession(alice, 0)
|
||||
r2, b2 := sessionCache.Get("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
r2, b2 := sessionCache.Get(rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"))
|
||||
assert.NotEmpty(t, r2)
|
||||
assert.True(t, b2)
|
||||
sessionCache.Flush()
|
||||
|
@ -122,28 +188,30 @@ func TestCacheSession(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeleteSession(t *testing.T) {
|
||||
m := &Session{ID: "77be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1", DownloadToken: "download123", PreviewToken: "preview123"}
|
||||
id := rnd.SessionID("77be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
m := &Session{ID: id, DownloadToken: "download123", PreviewToken: "preview123"}
|
||||
CacheSession(m, time.Hour)
|
||||
r, _ := sessionCache.Get("77be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
r, _ := sessionCache.Get(id)
|
||||
assert.NotEmpty(t, r)
|
||||
DeleteSession(m)
|
||||
r2, _ := sessionCache.Get("77be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
r2, _ := sessionCache.Get(id)
|
||||
assert.Empty(t, r2)
|
||||
}
|
||||
|
||||
func TestDeleteFromSessionCache(t *testing.T) {
|
||||
id := rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
sessionCache.Flush()
|
||||
bob := FindSessionByRefID("sessxkkcabce")
|
||||
CacheSession(bob, time.Hour)
|
||||
r, b := sessionCache.Get("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
r, b := sessionCache.Get(id)
|
||||
assert.NotEmpty(t, r)
|
||||
assert.True(t, b)
|
||||
DeleteFromSessionCache("")
|
||||
r2, b2 := sessionCache.Get("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
r2, b2 := sessionCache.Get(id)
|
||||
assert.NotEmpty(t, r2)
|
||||
assert.True(t, b2)
|
||||
DeleteFromSessionCache("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
r3, b3 := sessionCache.Get("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1")
|
||||
DeleteFromSessionCache(id)
|
||||
r3, b3 := sessionCache.Get(id)
|
||||
assert.Empty(t, r3)
|
||||
assert.False(t, b3)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package entity
|
|||
import (
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
type SessionMap map[string]Session
|
||||
|
@ -25,7 +26,8 @@ func (m SessionMap) Pointer(name string) *Session {
|
|||
|
||||
var SessionFixtures = SessionMap{
|
||||
"alice": {
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0",
|
||||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"),
|
||||
RefID: "sessxkkcabcd",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
|
@ -34,7 +36,8 @@ var SessionFixtures = SessionMap{
|
|||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
},
|
||||
"alice_token": {
|
||||
ID: "bb8658e779403ae524a188712470060f050054324a8b104e",
|
||||
authToken: "bb8658e779403ae524a188712470060f050054324a8b104e",
|
||||
ID: rnd.SessionID("bb8658e779403ae524a188712470060f050054324a8b104e"),
|
||||
RefID: "sess34q3hael",
|
||||
SessTimeout: -1,
|
||||
SessExpires: UnixTime() + UnixDay,
|
||||
|
@ -47,7 +50,8 @@ var SessionFixtures = SessionMap{
|
|||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
},
|
||||
"alice_token_scope": {
|
||||
ID: "778f0f7d80579a072836c65b786145d6e0127505194cc51e",
|
||||
authToken: "778f0f7d80579a072836c65b786145d6e0127505194cc51e",
|
||||
ID: rnd.SessionID("778f0f7d80579a072836c65b786145d6e0127505194cc51e"),
|
||||
RefID: "sessjr0ge18d",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixDay,
|
||||
|
@ -61,7 +65,8 @@ var SessionFixtures = SessionMap{
|
|||
DownloadToken: "64ydcbom",
|
||||
},
|
||||
"bob": {
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1",
|
||||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"),
|
||||
RefID: "sessxkkcabce",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
|
@ -70,7 +75,8 @@ var SessionFixtures = SessionMap{
|
|||
UserName: UserFixtures.Pointer("bob").UserName,
|
||||
},
|
||||
"unauthorized": {
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2",
|
||||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"),
|
||||
RefID: "sessxkkcabcf",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
|
@ -79,7 +85,8 @@ var SessionFixtures = SessionMap{
|
|||
UserName: UserFixtures.Pointer("unauthorized").UserName,
|
||||
},
|
||||
"visitor": {
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3",
|
||||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3"),
|
||||
RefID: "sessxkkcabcg",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
|
@ -93,7 +100,8 @@ var SessionFixtures = SessionMap{
|
|||
},
|
||||
},
|
||||
"visitor_token_metrics": {
|
||||
ID: "4ebe1048a7384e1e6af2930b5b6f29795ffab691df47a488",
|
||||
authToken: "4ebe1048a7384e1e6af2930b5b6f29795ffab691df47a488",
|
||||
ID: rnd.SessionID("4ebe1048a7384e1e6af2930b5b6f29795ffab691df47a488"),
|
||||
RefID: "sessaae5cxun",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
|
@ -105,7 +113,8 @@ var SessionFixtures = SessionMap{
|
|||
UserName: Visitor.UserName,
|
||||
},
|
||||
"friend": {
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac4",
|
||||
authToken: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac4",
|
||||
ID: rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac4"),
|
||||
RefID: "sessxkkcabch",
|
||||
SessTimeout: UnixDay * 3,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
|
@ -114,7 +123,8 @@ var SessionFixtures = SessionMap{
|
|||
UserName: UserFixtures.Pointer("friend").UserName,
|
||||
},
|
||||
"token_metrics": {
|
||||
ID: "9d8b8801ffa23eb52e08ca7766283799ddfd8dd368208a9b",
|
||||
authToken: "9d8b8801ffa23eb52e08ca7766283799ddfd8dd368208a9b",
|
||||
ID: rnd.SessionID("9d8b8801ffa23eb52e08ca7766283799ddfd8dd368208a9b"),
|
||||
RefID: "sessgh6gjuo1",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
|
@ -128,7 +138,8 @@ var SessionFixtures = SessionMap{
|
|||
DownloadToken: "vgln2ffb",
|
||||
},
|
||||
"token_settings": {
|
||||
ID: "3f9684f7d3dd3d5b84edd43289c7fb5ca32ee73bd0233237",
|
||||
authToken: "3f9684f7d3dd3d5b84edd43289c7fb5ca32ee73bd0233237",
|
||||
ID: rnd.SessionID("3f9684f7d3dd3d5b84edd43289c7fb5ca32ee73bd0233237"),
|
||||
RefID: "sessyugn54so",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
|
|
|
@ -10,7 +10,8 @@ func TestSessionMap_Get(t *testing.T) {
|
|||
t.Run("Alice", func(t *testing.T) {
|
||||
r := SessionFixtures.Get("alice")
|
||||
assert.Equal(t, "alice", r.UserName)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", r.ID)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", r.AuthToken())
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", r.ID)
|
||||
assert.IsType(t, Session{}, r)
|
||||
})
|
||||
|
||||
|
@ -25,7 +26,8 @@ func TestSessionMap_Get(t *testing.T) {
|
|||
func TestSessionMap_Pointer(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
r := SessionFixtures.Pointer("alice")
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", r.ID)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", r.AuthToken())
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", r.ID)
|
||||
assert.Equal(t, "alice", r.UserName)
|
||||
assert.IsType(t, &Session{}, r)
|
||||
})
|
||||
|
|
|
@ -98,7 +98,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
|||
// Login credentials provided?
|
||||
if f.HasCredentials() {
|
||||
if m.IsRegistered() {
|
||||
m.RegenerateID()
|
||||
m.Regenerate()
|
||||
}
|
||||
|
||||
user, provider, err = Auth(f, m, c)
|
||||
|
|
|
@ -4,10 +4,11 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/internal/server/header"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
|
@ -15,6 +16,7 @@ func TestNewSession(t *testing.T) {
|
|||
t.Run("NoSessionData", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour*6)
|
||||
|
||||
assert.True(t, rnd.IsAuthToken(m.AuthToken()))
|
||||
assert.True(t, rnd.IsSessionID(m.ID))
|
||||
assert.False(t, m.CreatedAt.IsZero())
|
||||
assert.False(t, m.UpdatedAt.IsZero())
|
||||
|
@ -27,6 +29,7 @@ func TestNewSession(t *testing.T) {
|
|||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m.SetData(NewSessionData())
|
||||
|
||||
assert.True(t, rnd.IsAuthToken(m.AuthToken()))
|
||||
assert.True(t, rnd.IsSessionID(m.ID))
|
||||
assert.False(t, m.CreatedAt.IsZero())
|
||||
assert.False(t, m.UpdatedAt.IsZero())
|
||||
|
@ -41,6 +44,7 @@ func TestNewSession(t *testing.T) {
|
|||
m := NewSession(UnixDay, UnixHour*6)
|
||||
m.SetData(data)
|
||||
|
||||
assert.True(t, rnd.IsAuthToken(m.AuthToken()))
|
||||
assert.True(t, rnd.IsSessionID(m.ID))
|
||||
assert.False(t, m.CreatedAt.IsZero())
|
||||
assert.False(t, m.UpdatedAt.IsZero())
|
||||
|
@ -116,42 +120,80 @@ func TestFindSessionByRefID(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSession_RegenerateID(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
func TestSession_Regenerate(t *testing.T) {
|
||||
t.Run("NewSession", func(t *testing.T) {
|
||||
m := NewSession(UnixDay, UnixHour)
|
||||
initialID := m.ID
|
||||
m.RegenerateID()
|
||||
m.Regenerate()
|
||||
finalID := m.ID
|
||||
assert.NotEqual(t, initialID, finalID)
|
||||
})
|
||||
t.Run("Empty ID", func(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
m := Session{ID: ""}
|
||||
initialID := m.ID
|
||||
m.RegenerateID()
|
||||
m.Regenerate()
|
||||
finalID := m.ID
|
||||
assert.NotEqual(t, initialID, finalID)
|
||||
})
|
||||
t.Run("Can't delete", func(t *testing.T) {
|
||||
t.Run("Existing", func(t *testing.T) {
|
||||
m := Session{ID: "1234567"}
|
||||
initialID := m.ID
|
||||
m.RegenerateID()
|
||||
m.Regenerate()
|
||||
finalID := m.ID
|
||||
assert.NotEqual(t, initialID, finalID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_AuthToken(t *testing.T) {
|
||||
t.Run("New", func(t *testing.T) {
|
||||
alice := SessionFixtures.Get("alice")
|
||||
sess := &Session{}
|
||||
assert.Equal(t, "", sess.ID)
|
||||
assert.Equal(t, "", sess.AuthToken())
|
||||
assert.False(t, rnd.IsSessionID(sess.ID))
|
||||
assert.False(t, rnd.IsAuthToken(sess.AuthToken()))
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
sess.Regenerate()
|
||||
assert.True(t, rnd.IsSessionID(sess.ID))
|
||||
assert.True(t, rnd.IsAuthToken(sess.AuthToken()))
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
sess.SetAuthToken(alice.AuthToken())
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", sess.ID)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", sess.AuthToken())
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
sess := SessionFixtures.Get("alice")
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", sess.ID)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", sess.AuthToken())
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
})
|
||||
t.Run("Find", func(t *testing.T) {
|
||||
alice := SessionFixtures.Get("alice")
|
||||
sess := FindSessionByRefID("sessxkkcabcd")
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", sess.ID)
|
||||
assert.Equal(t, "", sess.AuthToken())
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
sess.SetAuthToken(alice.AuthToken())
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", sess.ID)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", sess.AuthToken())
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_Create(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
m := FindSessionByRefID("sessxkkcxxxx")
|
||||
assert.Empty(t, m)
|
||||
s := &Session{
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7xxx",
|
||||
UserName: "charles",
|
||||
SessExpires: UnixDay * 3,
|
||||
SessTimeout: UnixTime() + UnixWeek,
|
||||
RefID: "sessxkkcxxxx",
|
||||
}
|
||||
|
||||
s.SetAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7xxx")
|
||||
|
||||
err := s.Create()
|
||||
|
||||
if err != nil {
|
||||
|
@ -162,35 +204,44 @@ func TestSession_Create(t *testing.T) {
|
|||
assert.Equal(t, "charles", m2.UserName)
|
||||
})
|
||||
t.Run("Invalid RefID", func(t *testing.T) {
|
||||
m, _ := FindSession("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7111")
|
||||
authToken := "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7111"
|
||||
id := rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7111")
|
||||
|
||||
m, _ := FindSession(id)
|
||||
|
||||
assert.Empty(t, m)
|
||||
|
||||
s := &Session{
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7111",
|
||||
UserName: "charles",
|
||||
SessExpires: UnixDay * 3,
|
||||
SessTimeout: UnixTime() + UnixWeek,
|
||||
RefID: "123",
|
||||
}
|
||||
|
||||
s.SetAuthToken(authToken)
|
||||
|
||||
err := s.Create()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m2, _ := FindSession("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7111")
|
||||
m2, _ := FindSession(id)
|
||||
|
||||
assert.NotEqual(t, "123", m2.RefID)
|
||||
})
|
||||
t.Run("ID already exists", func(t *testing.T) {
|
||||
authToken := "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"
|
||||
|
||||
s := &Session{
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0",
|
||||
UserName: "charles",
|
||||
SessExpires: UnixDay * 3,
|
||||
SessTimeout: UnixTime() + UnixWeek,
|
||||
RefID: "sessxkkcxxxx",
|
||||
}
|
||||
|
||||
s.SetAuthToken(authToken)
|
||||
|
||||
err := s.Create()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
@ -201,13 +252,14 @@ func TestSession_Save(t *testing.T) {
|
|||
m := FindSessionByRefID("sessxkkcxxxy")
|
||||
assert.Empty(t, m)
|
||||
s := &Session{
|
||||
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7xxy",
|
||||
UserName: "chris",
|
||||
SessExpires: UnixDay * 3,
|
||||
SessTimeout: UnixTime() + UnixWeek,
|
||||
RefID: "sessxkkcxxxy",
|
||||
}
|
||||
|
||||
s.SetAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7xxy")
|
||||
|
||||
err := s.Save()
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -1766,7 +1766,7 @@ func TestUser_DeleteSessions(t *testing.T) {
|
|||
t.Run("alice", func(t *testing.T) {
|
||||
m := FindLocalUser("alice")
|
||||
|
||||
assert.Equal(t, 0, m.DeleteSessions([]string{"69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"}))
|
||||
assert.Equal(t, 0, m.DeleteSessions([]string{rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")}))
|
||||
assert.Equal(t, 1, m.DeleteSessions([]string{}))
|
||||
})
|
||||
}
|
||||
|
@ -1792,7 +1792,6 @@ func TestUser_RegenerateTokens(t *testing.T) {
|
|||
|
||||
assert.NotEqual(t, preview, Admin.PreviewToken)
|
||||
assert.NotEqual(t, download, Admin.DownloadToken)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,10 @@ func Session(id string) (result entity.Session, err error) {
|
|||
return result, fmt.Errorf("invalid session id")
|
||||
} else if rnd.IsRefID(id) {
|
||||
err = Db().Where("ref_id = ?", id).First(&result).Error
|
||||
} else {
|
||||
} else if rnd.IsSessionID(id) {
|
||||
err = Db().Where("id LIKE ?", id).First(&result).Error
|
||||
} else {
|
||||
err = Db().Where("id LIKE ?", rnd.SessionID(id)).First(&result).Error
|
||||
}
|
||||
|
||||
return result, err
|
||||
|
@ -32,6 +34,8 @@ func Sessions(limit, offset int, sortOrder, search string) (result entity.Sessio
|
|||
stmt = stmt.Where("sess_expires > 0 AND sess_expires < ?", entity.UnixTime())
|
||||
} else if rnd.IsSessionID(search) {
|
||||
stmt = stmt.Where("id = ?", search)
|
||||
} else if rnd.IsAuthToken(search) {
|
||||
stmt = stmt.Where("id = ?", rnd.SessionID(search))
|
||||
} else if rnd.IsUID(search, entity.UserUID) {
|
||||
stmt = stmt.Where("user_uid = ?", search)
|
||||
} else if search != "" {
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
|
@ -22,7 +24,7 @@ func TestSession(t *testing.T) {
|
|||
} else {
|
||||
t.Logf("session: %#v", result)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", result.ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"), result.ID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", result.UserUID)
|
||||
assert.Equal(t, "alice", result.UserName)
|
||||
}
|
||||
|
@ -33,7 +35,7 @@ func TestSession(t *testing.T) {
|
|||
} else {
|
||||
t.Logf("session: %#v", result)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1", result.ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"), result.ID)
|
||||
assert.Equal(t, "uqxc08w3d0ej2283", result.UserUID)
|
||||
assert.Equal(t, "bob", result.UserName)
|
||||
}
|
||||
|
@ -72,7 +74,7 @@ func TestSessions(t *testing.T) {
|
|||
t.Logf("sessions: %#v", results)
|
||||
assert.LessOrEqual(t, 1, len(results))
|
||||
if len(results) > 0 {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", results[0].ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"), results[0].ID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", results[0].UserUID)
|
||||
assert.Equal(t, "alice", results[0].UserName)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestSessions(t *testing.T) {
|
||||
|
@ -40,7 +41,7 @@ func TestSessions(t *testing.T) {
|
|||
t.Logf("sessions: %#v", results)
|
||||
assert.LessOrEqual(t, 1, len(results))
|
||||
if len(results) > 0 {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", results[0].ID)
|
||||
assert.Equal(t, rnd.SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"), results[0].ID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", results[0].UserUID)
|
||||
assert.Equal(t, "alice", results[0].UserName)
|
||||
}
|
||||
|
|
|
@ -3,4 +3,6 @@ package header
|
|||
const (
|
||||
SessionID = "X-Session-ID"
|
||||
Authorization = "Authorization" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
||||
BasicAuth = "Basic"
|
||||
BearerAuth = "Bearer"
|
||||
)
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// ID returns a random 48-character session id string.
|
||||
func ID() string {
|
||||
return rnd.SessionID()
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewID(t *testing.T) {
|
||||
for n := 0; n < 5; n++ {
|
||||
id := ID()
|
||||
t.Logf("id: %s", id)
|
||||
assert.Equal(t, 48, len(id))
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ func TestSession_Exists(t *testing.T) {
|
|||
|
||||
t.Logf("ID: %s", sess.ID)
|
||||
|
||||
assert.Equal(t, 48, len(sess.ID))
|
||||
assert.Equal(t, 64, len(sess.ID))
|
||||
assert.True(t, s.Exists(sess.ID))
|
||||
|
||||
err = s.Delete(sess.ID)
|
||||
|
|
|
@ -2,24 +2,32 @@ package session
|
|||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
var Public *entity.Session
|
||||
var PublicID = "234200000000000000000000000000000000000000000000"
|
||||
// PublicAuthToken is a static authentication token used in public mode.
|
||||
var PublicAuthToken = "234200000000000000000000000000000000000000000000"
|
||||
|
||||
// PublicID is the SHA256 hash of the PublicAuthToken:
|
||||
// a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f
|
||||
var PublicID = rnd.SessionID(PublicAuthToken)
|
||||
|
||||
// public references the existing public mode session entity.
|
||||
var public *entity.Session
|
||||
|
||||
// Public returns a client session for use in public mode.
|
||||
func (s *Session) Public() *entity.Session {
|
||||
if Public == nil {
|
||||
if public == nil {
|
||||
// Do nothing.
|
||||
} else if !Public.Expired() {
|
||||
return Public
|
||||
} else if !public.Expired() {
|
||||
return public
|
||||
}
|
||||
|
||||
Public = entity.NewSession(0, 0)
|
||||
Public.ID = PublicID
|
||||
Public.AuthMethod = "public"
|
||||
Public.SetUser(&entity.Admin)
|
||||
Public.CacheDuration(-1)
|
||||
public = entity.NewSession(0, 0)
|
||||
public.SetAuthToken(PublicAuthToken)
|
||||
public.AuthMethod = "public"
|
||||
public.SetUser(&entity.Admin)
|
||||
public.CacheDuration(-1)
|
||||
|
||||
return Public
|
||||
return public
|
||||
}
|
||||
|
|
|
@ -14,9 +14,11 @@ import (
|
|||
func TestSession_Save(t *testing.T) {
|
||||
s := New(config.TestConfig())
|
||||
|
||||
id := ID()
|
||||
authToken := rnd.AuthToken()
|
||||
id := rnd.SessionID(authToken)
|
||||
|
||||
assert.Equal(t, 48, len(id))
|
||||
assert.Equal(t, 48, len(authToken))
|
||||
assert.Equal(t, 64, len(id))
|
||||
assert.Falsef(t, s.Exists(id), "session %s should not exist", clean.LogQuote(id))
|
||||
|
||||
var err error
|
||||
|
@ -32,8 +34,8 @@ func TestSession_Save(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 48, len(m.ID))
|
||||
assert.Truef(t, s.Exists(m.ID), "session %s should exist", clean.LogQuote(id))
|
||||
assert.Equal(t, 64, len(m.ID))
|
||||
assert.Truef(t, s.Exists(m.ID), "session %s should exist", clean.LogQuote(m.ID))
|
||||
|
||||
newData := &entity.SessionData{
|
||||
Shares: entity.UIDs{"a000000000000001"},
|
||||
|
@ -65,5 +67,6 @@ func TestSession_Create(t *testing.T) {
|
|||
assert.NotEmpty(t, sess)
|
||||
assert.NotEmpty(t, sess.ID)
|
||||
assert.NotEmpty(t, sess.RefID)
|
||||
assert.True(t, rnd.IsAuthToken(sess.AuthToken()))
|
||||
assert.True(t, rnd.IsSessionID(sess.ID))
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"log"
|
||||
)
|
||||
|
||||
// SessionID returns a new session id.
|
||||
func SessionID() string {
|
||||
// AuthToken returns a new session id.
|
||||
func AuthToken() string {
|
||||
b := make([]byte, 24)
|
||||
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
@ -17,11 +17,25 @@ func SessionID() string {
|
|||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// IsSessionID checks if the string is a session id.
|
||||
func IsSessionID(s string) bool {
|
||||
// IsAuthToken checks if the string is a session id.
|
||||
func IsAuthToken(s string) bool {
|
||||
if len(s) != 48 {
|
||||
return false
|
||||
}
|
||||
|
||||
return IsHex(s)
|
||||
}
|
||||
|
||||
// SessionID returns the hashed session id string.
|
||||
func SessionID(s string) string {
|
||||
return Sha256([]byte(s))
|
||||
}
|
||||
|
||||
// IsSessionID checks if the string is a session id string.
|
||||
func IsSessionID(s string) bool {
|
||||
if len(s) != 64 {
|
||||
return false
|
||||
}
|
||||
|
||||
return IsHex(s)
|
||||
}
|
||||
|
|
|
@ -6,10 +6,37 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuthToken(t *testing.T) {
|
||||
result := AuthToken()
|
||||
assert.Equal(t, 48, len(result))
|
||||
assert.True(t, IsAuthToken(result))
|
||||
assert.True(t, IsHex(result))
|
||||
}
|
||||
|
||||
func TestIsAuthToken(t *testing.T) {
|
||||
assert.True(t, IsAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.True(t, IsAuthToken(AuthToken()))
|
||||
assert.True(t, IsAuthToken(AuthToken()))
|
||||
assert.False(t, IsAuthToken(SessionID(AuthToken())))
|
||||
assert.False(t, IsAuthToken(SessionID(AuthToken())))
|
||||
assert.False(t, IsAuthToken("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.False(t, IsAuthToken("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.False(t, IsAuthToken(""))
|
||||
}
|
||||
|
||||
func TestSessionID(t *testing.T) {
|
||||
result := SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2")
|
||||
assert.Equal(t, 64, len(result))
|
||||
assert.Equal(t, "f22383a703805a031a9835c8c6b6dafb793a21e8f33d0b4887b4ec9bd7ac8cd5", result)
|
||||
}
|
||||
|
||||
func TestIsSessionID(t *testing.T) {
|
||||
assert.True(t, IsSessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.True(t, IsSessionID(SessionID()))
|
||||
assert.True(t, IsSessionID(SessionID()))
|
||||
assert.False(t, IsSessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.False(t, IsSessionID(AuthToken()))
|
||||
assert.False(t, IsSessionID(AuthToken()))
|
||||
assert.True(t, IsSessionID(SessionID(AuthToken())))
|
||||
assert.True(t, IsSessionID(SessionID(AuthToken())))
|
||||
assert.False(t, IsSessionID("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.False(t, IsSessionID("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.False(t, IsSessionID(""))
|
||||
}
|
||||
|
|
22
pkg/rnd/sha.go
Normal file
22
pkg/rnd/sha.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package rnd
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Sha224 returns the SHA224 checksum of the byte slice as a hex string.
|
||||
func Sha224(b []byte) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum224(b))
|
||||
}
|
||||
|
||||
// Sha256 returns the SHA256 checksum of the byte slice as a hex string.
|
||||
func Sha256(b []byte) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256(b))
|
||||
}
|
||||
|
||||
// Sha512 returns the SHA512 checksum of the byte slice as a hex string.
|
||||
func Sha512(b []byte) string {
|
||||
return fmt.Sprintf("%x", sha512.Sum512(b))
|
||||
}
|
67
pkg/rnd/sha_test.go
Normal file
67
pkg/rnd/sha_test.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSha224(t *testing.T) {
|
||||
t.Run("Nil", func(t *testing.T) {
|
||||
s := Sha224(nil)
|
||||
t.Logf("Sha224(nil): %s", s)
|
||||
assert.NotEmpty(t, s)
|
||||
assert.True(t, IsHex(s))
|
||||
assert.Equal(t, 56, len(s))
|
||||
|
||||
})
|
||||
t.Run("HelloWorld", func(t *testing.T) {
|
||||
s := Sha224([]byte("hello world\n"))
|
||||
t.Logf("Sha224(HelloWorld): %s", s)
|
||||
assert.NotEmpty(t, s)
|
||||
assert.True(t, IsHex(s))
|
||||
assert.Equal(t, "95041dd60ab08c0bf5636d50be85fe9790300f39eb84602858a9b430", s)
|
||||
assert.Equal(t, 56, len(s))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestSha256(t *testing.T) {
|
||||
t.Run("Nil", func(t *testing.T) {
|
||||
s := Sha256(nil)
|
||||
t.Logf("Sha256(nil): %s", s)
|
||||
assert.NotEmpty(t, s)
|
||||
assert.True(t, IsHex(s))
|
||||
assert.Equal(t, 64, len(s))
|
||||
|
||||
})
|
||||
t.Run("HelloWorld", func(t *testing.T) {
|
||||
s := Sha256([]byte("hello world\n"))
|
||||
t.Logf("Sha256(HelloWorld): %s", s)
|
||||
assert.NotEmpty(t, s)
|
||||
assert.True(t, IsHex(s))
|
||||
assert.Equal(t, "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", s)
|
||||
assert.Equal(t, 64, len(s))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestSha512(t *testing.T) {
|
||||
t.Run("Nil", func(t *testing.T) {
|
||||
s := Sha512(nil)
|
||||
t.Logf("Sha512(nil): %s", s)
|
||||
assert.NotEmpty(t, s)
|
||||
assert.True(t, IsHex(s))
|
||||
assert.Equal(t, 128, len(s))
|
||||
|
||||
})
|
||||
t.Run("HelloWorld", func(t *testing.T) {
|
||||
s := Sha512([]byte("hello world\n"))
|
||||
t.Logf("Sha512(HelloWorld): %s", s)
|
||||
assert.NotEmpty(t, s)
|
||||
assert.True(t, IsHex(s))
|
||||
assert.Equal(t, "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593", s)
|
||||
assert.Equal(t, 128, len(s))
|
||||
|
||||
})
|
||||
}
|
|
@ -39,7 +39,7 @@ func IdType(id string) (Type, byte) {
|
|||
return TypeSHA1, PrefixNone
|
||||
case IsRefID(id):
|
||||
return TypeRefID, PrefixNone
|
||||
case IsSessionID(id):
|
||||
case IsAuthToken(id):
|
||||
return TypeSessionID, PrefixNone
|
||||
case ValidateCrcToken(id):
|
||||
return TypeCrcToken, PrefixNone
|
||||
|
|
Loading…
Reference in a new issue