Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #24342: Merge user-management feature branch to 7.3 #5441

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion webapp/sources/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ limitations under the License.
<!-- ==================================== PROJECT INFORMATION ==================================== -->

<description>
Rudder helps you continualy configure and check compliance of your infrastructure.
Rudder helps you continually configure and check compliance of your infrastructure.
</description>

<organization>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,31 @@ ALTER database rudder SET standard_conforming_strings=true;

CREATE SEQUENCE serial START 101;

CREATE TABLE Users (
id text PRIMARY KEY NOT NULL CHECK (id <> '')
, creationDate timestamp with time zone NOT NULL
, status text NOT NULL
, managedBy text NOT NULL CHECK (managedBy <> '')
, name text
, email text
, lastLogin timestamp with time zone
, statusHistory jsonb
, otherInfo jsonb -- general additional user info
);

CREATE TABLE UserSessions (
userId text NOT NULL CHECK (userId <> '')
, sessionId text NOT NULL CHECK (sessionId <> '')
, creationDate timestamp with time zone NOT NULL
, authMethod text
, permissions text[]
, authz text[]
, endDate timestamp with time zone
, endCause text
, PRIMARY KEY(userId, sessionId)
);


CREATE TABLE RudderSysEvents (
id bigint PRIMARY KEY default nextval('serial')
, executionDate timestamp with time zone NOT NULL
Expand Down Expand Up @@ -408,4 +433,4 @@ CREATE TABLE CampaignEvents (
);


CREATE INDEX event_state_index ON CampaignEvents ((state->>'value'));
CREATE INDEX event_state_index ON CampaignEvents ((state->>'value'));
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ final case object HttpAction {

def values: Set[HttpAction] = ca.mrvisser.sealerate.values[HttpAction]

// sort the action by the importance of the action, please DON'T FORGET TO ADD NEW VALUES HERE
implicit val orderingHttpAction: Ordering[HttpAction] = new Ordering[HttpAction] {
val order = List(HEAD, GET, PUT, POST, DELETE)
override def compare(x: HttpAction, y: HttpAction): Int = {
order.indexOf(x) - order.indexOf(y)
}
}

def parse(action: String): Either[String, HttpAction] = {
val lower = action.toLowerCase()
values.find(_.name == lower).toRight(s"Action '${action}' is not recognized as a supported HTTP action")
Expand Down Expand Up @@ -133,7 +141,8 @@ object AclPathSegment {
*/
sealed trait AclPath extends Any {
def value: String = parts.toList.map(_.value).mkString("/")
def parts: NonEmptyList[AclPathSegment]
def parts: NonEmptyList[AclPathSegment]
def display: String
}

object AclPath {
Expand All @@ -142,10 +151,12 @@ object AclPath {
// in our simpler case)
final case class FullPath(segments: NonEmptyList[AclPathSegment]) extends AnyVal with AclPath {
def parts = segments
def display: String = "/" + value
}
// only the root is given, and the path ends with "**". It can even be only "**"
final case class Root(segments: List[AclPathSegment]) extends AnyVal with AclPath {
def parts: NonEmptyList[AclPathSegment] = NonEmptyList.ofInitLast(segments, AclPathSegment.DoubleWildcard)
def parts: NonEmptyList[AclPathSegment] = NonEmptyList.ofInitLast(segments, AclPathSegment.DoubleWildcard)
def display: String = "/"
}

// parse a path to an acl path.
Expand Down Expand Up @@ -206,7 +217,7 @@ object AclPath {
* is no authorization for that path.
*/
final case class ApiAclElement(path: AclPath, actions: Set[HttpAction]) {
def display: String = path.value + ":" + actions.map(_.name.toUpperCase()).mkString("[", ",", "]")
def display: String = path.display + ":" + actions.toList.sorted.map(_.name.toUpperCase()).mkString("[", ",", "]")
}

sealed trait ApiAuthorizationKind { def name: String }
Expand Down Expand Up @@ -246,7 +257,10 @@ object ApiAuthorization {
case object None extends ApiAuthorization { override val kind = ApiAuthorizationKind.None }
case object RW extends ApiAuthorization { override val kind = ApiAuthorizationKind.RW }
case object RO extends ApiAuthorization { override val kind = ApiAuthorizationKind.RO }
final case class ACL(acl: List[ApiAclElement]) extends ApiAuthorization { override def kind = ApiAuthorizationKind.ACL }
final case class ACL(acl: List[ApiAclElement]) extends ApiAuthorization {
override def kind = ApiAuthorizationKind.ACL
def debugString: String = acl.map(_.display).mkString(";")
}

/**
* An authorization object with ALL authorization,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
*************************************************************************************
* Copyright 2023 Normation SAS
*************************************************************************************
*
* This file is part of Rudder.
*
* Rudder is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In accordance with the terms of section 7 (7. Additional Terms.) of
* the GNU General Public License version 3, the copyright holders add
* the following Additional permissions:
* Notwithstanding to the terms of section 5 (5. Conveying Modified Source
* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General
* Public License version 3, when you create a Related Module, this
* Related Module is not considered as a part of the work and may be
* distributed under the license agreement of your choice.
* A "Related Module" means a set of sources files including their
* documentation that, without modification of the Source Code, enables
* supplementary functions or services in addition to those offered by
* the Software.
*
* Rudder is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Rudder. If not, see <http://www.gnu.org/licenses/>.

*
*************************************************************************************
*/

package com.normation.rudder.batch

import com.normation.rudder.domain.eventlog.RudderEventActor
import com.normation.rudder.domain.logger.ApplicationLoggerPure
import com.normation.rudder.users._
import com.normation.utils.CronParser._
import com.normation.utils.DateFormaterService
import com.normation.zio._
import cron4s.CronExpr
import org.joda.time.DateTime
import zio._

/**
* A scheduler which runs user accounts and user sessions clean-up:
* - disable/delete inactive accounts,
* - purge old deleted accounts,
* - delete user sessions
*
* It use a cron-like config parsed by cron4s.
*
* Code derived from https://github.com/centic9/jgit-cookbook/blob/master/src/main/java/org/dstadler/jgit/porcelain/CollectGarbage.java
*/
class CleanupUsers(
userRepository: UserRepository,
optCron: Option[CronExpr],
disableInactive: Duration,
deleteInactive: Duration,
purgeDeleted: Duration,
purgeSessions: Duration,
localBackends: List[String]
) {

val logger = ApplicationLoggerPure.User

/*
* Clean-up has different steps:
* - disable accounts inactive since `disableInactive`
* - delete accounts inactive since `deleteInactive`
* - purge account deleted since `purgeDeleted`
* - purge sessions older than `purgeSessions`
*
* For local backend, we don't want to "delete" accounts, since they would be recreated
* at next rudder reboot. They need to be removed from the local param (`rudder-user.xml` or `rudder-web.properties`)
* or via the plugin and then purged like others.
*
*/
val cleanup: UIO[Unit] = for {
t0 <- currentTimeMillis
d = new DateTime(t0)
t = EventTrace(RudderEventActor, d)
_ <- userRepository
.disable(Nil, Some(d.minus(disableInactive.toMillis)), Nil, t)
.catchAll(err =>
logger.error(s"Error when disabling user accounts inactive since '${disableInactive.toString}': ${err.fullMsg}")
)
_ <- userRepository
.delete(Nil, Some(d.minus(deleteInactive.toMillis)), localBackends, t)
.flatMap(deletedUsers =>
ApplicationLoggerPure.User.info(s"Following users status changed to 'deleted': '${deletedUsers.mkString("' ,'")}'")
)
.catchAll(err =>
logger.error(s"Error when deleting user accounts inactive since '${deleteInactive.toString}': ${err.fullMsg}")
)
_ <- userRepository
.purge(Nil, Some(d.minus(purgeDeleted.toMillis)), Nil, t)
.catchAll(err =>
logger.error(s"Error when purging user accounts deleted since '${purgeDeleted.toString}': ${err.fullMsg}")
)
dos = d.minus(purgeSessions.toMillis)
_ <-
userRepository
.deleteOldSessions(dos)
.flatMap(deletedUsers =>
ApplicationLoggerPure.User.info(s"Log of user sessions older than ${DateFormaterService.serialize(dos)} were deleted")
)
.catchAll(err => {
logger.error(
s"Error when purging user sessions older than '${DateFormaterService.serialize(d)}': ${err.fullMsg}"
)
})
t1 <- currentTimeMillis
_ <- logger.info(s"Cleaning user accounts and sessions performed in ${new org.joda.time.Duration(t1 - t0).toString}")
} yield ()

// Must not fail.
val prog: UIO[Unit] = optCron match {
case None =>
logger.info(s"Disable automatic cleaning of user accounts and user sessions (schedule: '${DISABLED}')")
case Some(cron) =>
val schedule = cron.toSchedule
logger.info(
s"Automatic cleaning of user accounts and sessions started (schedule 'sec min h dayMonth month DayWeek': '${cron.toString}')"
) *>
cleanup.schedule(schedule).unit
}

// start cron
def start() = {
ZioRuntime.unsafeRun(prog.forkDaemon)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ object ApplicationLoggerPure extends NamedZioLogger {
object Authz extends NamedZioLogger {
def loggerName: String = parent.loggerName + ".authorization"
}

object User extends NamedZioLogger {
def loggerName = parent.loggerName + ".user"
}
}

object ApiLogger extends Logger {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,6 @@ class ItemArchiveManagerImpl(
modId
)
} yield {

asyncDeploymentAgent ! AutomaticStartDeployment(modId, actor)
archiveId
}
Expand Down