Merge pull request #324 from stianst/master

Audit configurable through admin console
This commit is contained in:
Stian Thorgersen 2014-04-07 18:06:43 +01:00
commit a197bda363
26 changed files with 509 additions and 92 deletions

View file

@ -141,6 +141,21 @@ module.config([ '$routeProvider', function($routeProvider) {
return RealmLoader(); return RealmLoader();
} }
}, },
controller : 'RealmAuditEventsCtrl'
})
.when('/realms/:realm/audit-settings', {
templateUrl : 'partials/realm-audit-config.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
},
auditConfig : function(RealmAuditLoader) {
return RealmAuditLoader();
}
},
controller : 'RealmAuditCtrl' controller : 'RealmAuditCtrl'
}) })
.when('/realms/:realm/auth-settings', { .when('/realms/:realm/auth-settings', {

View file

@ -1022,7 +1022,65 @@ module.controller('RealmAuthSettingsDetailCtrl', function($scope, $routeParams,
}; };
}); });
module.controller('RealmAuditCtrl', function($scope, RealmAudit, realm) { module.controller('RealmAuditCtrl', function($scope, auditConfig, RealmAudit, RealmAuditEvents, realm, serverInfo, $location, Notifications, TimeUnit, Dialog) {
$scope.realm = realm;
$scope.auditConfig = auditConfig;
$scope.auditConfig.expirationUnit = TimeUnit.autoUnit(auditConfig.auditExpiration);
if ($scope.auditConfig.expirationUnit) {
$scope.auditConfig.expirationUnit = 'Hours';
}
$scope.auditConfig.auditExpiration = TimeUnit.toUnit(auditConfig.auditExpiration, $scope.auditConfig.expirationUnit);
$scope.$watch('auditConfig.expirationUnit', function(to, from) {
if ($scope.auditConfig.auditExpiration) {
$scope.auditConfig.auditExpiration = TimeUnit.convert($scope.auditConfig.auditExpiration, from, to);
}
});
$scope.auditListeners = serverInfo.auditListeners;
var oldCopy = angular.copy($scope.auditConfig);
$scope.changed = false;
$scope.$watch('auditConfig', function() {
if (!angular.equals($scope.auditConfig, oldCopy)) {
$scope.changed = true;
}
}, true);
$scope.save = function() {
$scope.changed = false;
var copy = angular.copy($scope.auditConfig)
delete copy['expirationUnit'];
copy.auditExpiration = TimeUnit.toSeconds($scope.auditConfig.auditExpiration, $scope.auditConfig.expirationUnit);
RealmAudit.update({
id : realm.realm
}, copy, function () {
$location.url("/realms/" + realm.realm + "/audit-settings");
Notifications.success("Your changes have been saved to the realm.");
});
};
$scope.reset = function() {
$scope.auditConfig = angular.copy(oldCopy);
$scope.changed = false;
};
$scope.clearAudit = function() {
Dialog.confirmDelete($scope.realm.realm, 'audit events', function() {
RealmAuditEvents.remove({ id : $scope.realm.realm }, function() {
Notifications.success("The audit events has been cleared.");
});
});
};
});
module.controller('RealmAuditEventsCtrl', function($scope, RealmAuditEvents, realm) {
$scope.realm = realm; $scope.realm = realm;
$scope.page = 0; $scope.page = 0;
@ -1038,8 +1096,7 @@ module.controller('RealmAuditCtrl', function($scope, RealmAudit, realm) {
delete $scope.query[i]; delete $scope.query[i];
} }
} }
console.debug($scope.query.first); $scope.events = RealmAuditEvents.query($scope.query);
$scope.events = RealmAudit.query($scope.query);
} }
$scope.firstPage = function() { $scope.firstPage = function() {

View file

@ -47,6 +47,14 @@ module.factory('RealmLoader', function(Loader, Realm, $route, $q) {
}); });
}); });
module.factory('RealmAuditLoader', function(Loader, RealmAudit, $route, $q) {
return Loader.get(RealmAudit, function() {
return {
id : $route.current.params.realm
}
});
});
module.factory('UserListLoader', function(Loader, User, $route, $q) { module.factory('UserListLoader', function(Loader, User, $route, $q) {
return Loader.query(User, function() { return Loader.query(User, function() {
return { return {

View file

@ -145,6 +145,16 @@ module.factory('Realm', function($resource) {
module.factory('RealmAudit', function($resource) { module.factory('RealmAudit', function($resource) {
return $resource(authUrl + '/rest/admin/realms/:id/audit', { return $resource(authUrl + '/rest/admin/realms/:id/audit', {
id : '@realm' id : '@realm'
}, {
update : {
method : 'PUT'
}
});
});
module.factory('RealmAuditEvents', function($resource) {
return $resource(authUrl + '/rest/admin/realms/:id/audit/events', {
id : '@realm'
}); });
}); });

View file

@ -0,0 +1,71 @@
<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
<div id="content-area" class="col-sm-9" role="main">
<ul class="nav nav-tabs nav-tabs-pf">
<li data-ng-class="(path[2] == 'audit') && 'active'"><a href="#/realms/{{realm.realm}}/audit">View</a></li>
<li data-ng-class="(path[2] == 'audit-settings') && 'active'"><a href="#/realms/{{realm.realm}}/audit-settings">Config</a></li>
</ul>
<div id="content">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li>
<li><a href="#/realms/{{realm.realm}}">Audit</a></li>
<li class="active">Social</li>
</ol>
<h2><span>{{realm.realm}}</span> Audit Config</h2>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageAudit">
<fieldset>
<div class="form-group" data-ng-show="access.manageAudit">
<label class="col-sm-2 control-label" for="password">Clear Audit</label>
<div class="col-sm-4">
<button class="btn btn-danger" type="submit" data-ng-click="clearAudit()" >Clear Audit</button>
</div>
</div>
</fieldset>
<fieldset>
<div class="form-group">
<label class="col-sm-2 control-label" for="enabled">Enabled</label>
<div class="col-sm-4">
<input ng-model="auditConfig.auditEnabled" name="enabled" id="enabled" onoffswitch />
</div>
</div>
<div class="form-group input-select">
<label class="col-sm-2 control-label" for="expiration">Expiration</label>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-2">
<input class="form-control" type="number"
data-ng-model="auditConfig.auditExpiration"
id="expiration" name="expiration"/>
</div>
<div class="col-sm-2 select-kc">
<select name="expirationUnit" data-ng-model="auditConfig.expirationUnit" >
<option>Hours</option>
<option>Days</option>
</select>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="auditListeners" class="control-label">Audit Listeners</label>
<div class="col-sm-4">
<select ui-select2 ng-model="auditConfig.auditListeners" data-placeholder="Select an action..." multiple>
<option ng-repeat="listener in auditListeners" value="{{listener}}">{{listener}}</option>
</select>
</div>
</div>
</fieldset>
<div class="pull-right form-actions" data-ng-show="access.manageAudit">
<button data-kc-reset data-ng-show="changed">Clear changes</button>
<button data-kc-save data-ng-show="changed">Save</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -1,6 +1,11 @@
<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div> <div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
<div id="content-area" class="col-sm-9" role="main"> <div id="content-area" class="col-sm-9" role="main">
<data-kc-navigation data-kc-current="social" data-kc-realm="realm.realm" data-kc-social="realm.social"></data-kc-navigation>
<ul class="nav nav-tabs nav-tabs-pf">
<li data-ng-class="(path[2] == 'audit') && 'active'"><a href="#/realms/{{realm.realm}}/audit">View</a></li>
<li data-ng-class="(path[2] == 'audit-settings') && 'active'"><a href="#/realms/{{realm.realm}}/audit-settings">Config</a></li>
</ul>
<div id="content"> <div id="content">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li> <li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li>

View file

@ -9,5 +9,5 @@
<li data-ng-show="access.viewApplications" data-ng-class="(path[2] == 'applications' || path[1] == 'application' || path[3] == 'applications') && 'active'"><a href="#/realms/{{realm.realm}}/applications">Applications</a></li> <li data-ng-show="access.viewApplications" data-ng-class="(path[2] == 'applications' || path[1] == 'application' || path[3] == 'applications') && 'active'"><a href="#/realms/{{realm.realm}}/applications">Applications</a></li>
<li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'oauth-clients' || path[1] == 'oauth-client') && 'active'"><a href="#/realms/{{realm.realm}}/oauth-clients">OAuth Clients</a></li> <li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'oauth-clients' || path[1] == 'oauth-client') && 'active'"><a href="#/realms/{{realm.realm}}/oauth-clients">OAuth Clients</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/revocation">Sessions</a></li> <li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/revocation">Sessions</a></li>
<li data-ng-show="access.viewAudit" data-ng-class="(path[2] == 'audit') && 'active'"><a href="#/realms/{{realm.realm}}/audit">Audit</a></li> <li data-ng-show="access.viewAudit" data-ng-class="(path[2] == 'audit' || path[2] == 'audit-settings') && 'active'"><a href="#/realms/{{realm.realm}}/audit">Audit</a></li>
</ul> </ul>

View file

@ -7,8 +7,8 @@ public interface AuditProvider extends AuditListener {
public EventQuery createQuery(); public EventQuery createQuery();
public void clear(); public void clear(String realmId);
public void clear(long olderThan); public void clear(String realmId, long olderThan);
} }

View file

@ -29,15 +29,15 @@ public class JpaAuditProvider implements AuditProvider {
} }
@Override @Override
public void clear() { public void clear(String realmId) {
beginTx(); beginTx();
em.createQuery("delete from EventEntity").executeUpdate(); em.createQuery("delete from EventEntity where realmId = :realmId").setParameter("realmId", realmId).executeUpdate();
} }
@Override @Override
public void clear(long olderThan) { public void clear(String realmId, long olderThan) {
beginTx(); beginTx();
em.createQuery("delete from EventEntity where time < :time").setParameter("time", olderThan).executeUpdate(); em.createQuery("delete from EventEntity where realmId = :realmId and time < :time").setParameter("realmId", realmId).setParameter("time", olderThan).executeUpdate();
} }
@Override @Override

View file

@ -1,5 +1,6 @@
package org.keycloak.audit.mongo; package org.keycloak.audit.mongo;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection; import com.mongodb.DBCollection;
import com.mongodb.DBObject; import com.mongodb.DBObject;
@ -27,13 +28,16 @@ public class MongoAuditProvider implements AuditProvider {
} }
@Override @Override
public void clear() { public void clear(String realmId) {
audit.remove(new BasicDBObject()); audit.remove(new BasicDBObject("realmId", realmId));
} }
@Override @Override
public void clear(long olderThan) { public void clear(String realmId, long olderThan) {
audit.remove(new BasicDBObject("time", new BasicDBObject("$lt", olderThan))); BasicDBObject q = new BasicDBObject();
q.put("realmId", realmId);
q.put("time", new BasicDBObject("$lt", olderThan));
audit.remove(q);
} }
@Override @Override

View file

@ -34,7 +34,8 @@ public abstract class AbstractAuditProviderTest {
@After @After
public void after() { public void after() {
provider.clear(); provider.clear("realmId");
provider.clear("realmId2");
provider.close(); provider.close();
factory.close(); factory.close();
} }
@ -51,6 +52,7 @@ public abstract class AbstractAuditProviderTest {
provider.onEvent(create("event", "realmId", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create("event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(newest, "event2", "realmId", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create(newest, "event2", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(newest, "event2", "realmId", "clientId", "userId2", "127.0.0.1", "error"));
provider.onEvent(create("event", "realmId2", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create("event", "realmId2", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(oldest, "event", "realmId", "clientId2", "userId", "127.0.0.1", "error")); provider.onEvent(create(oldest, "event", "realmId", "clientId2", "userId", "127.0.0.1", "error"));
provider.onEvent(create("event", "realmId", "clientId", "userId2", "127.0.0.1", "error")); provider.onEvent(create("event", "realmId", "clientId", "userId2", "127.0.0.1", "error"));
@ -58,19 +60,19 @@ public abstract class AbstractAuditProviderTest {
provider.close(); provider.close();
provider = factory.create(); provider = factory.create();
Assert.assertEquals(4, provider.createQuery().client("clientId").getResultList().size()); Assert.assertEquals(5, provider.createQuery().client("clientId").getResultList().size());
Assert.assertEquals(4, provider.createQuery().realm("realmId").getResultList().size()); Assert.assertEquals(5, provider.createQuery().realm("realmId").getResultList().size());
Assert.assertEquals(4, provider.createQuery().event("event").getResultList().size()); Assert.assertEquals(4, provider.createQuery().event("event").getResultList().size());
Assert.assertEquals(5, provider.createQuery().event("event", "event2").getResultList().size()); Assert.assertEquals(6, provider.createQuery().event("event", "event2").getResultList().size());
Assert.assertEquals(4, provider.createQuery().user("userId").getResultList().size()); Assert.assertEquals(4, provider.createQuery().user("userId").getResultList().size());
Assert.assertEquals(1, provider.createQuery().user("userId").event("event2").getResultList().size()); Assert.assertEquals(1, provider.createQuery().user("userId").event("event2").getResultList().size());
Assert.assertEquals(2, provider.createQuery().maxResults(2).getResultList().size()); Assert.assertEquals(2, provider.createQuery().maxResults(2).getResultList().size());
Assert.assertEquals(1, provider.createQuery().firstResult(4).getResultList().size()); Assert.assertEquals(1, provider.createQuery().firstResult(5).getResultList().size());
Assert.assertEquals(newest, provider.createQuery().maxResults(1).getResultList().get(0).getTime()); Assert.assertEquals(newest, provider.createQuery().maxResults(1).getResultList().get(0).getTime());
Assert.assertEquals(oldest, provider.createQuery().firstResult(4).maxResults(1).getResultList().get(0).getTime()); Assert.assertEquals(oldest, provider.createQuery().firstResult(5).maxResults(1).getResultList().get(0).getTime());
} }
@Test @Test
@ -79,13 +81,14 @@ public abstract class AbstractAuditProviderTest {
provider.onEvent(create(System.currentTimeMillis() - 20000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create(System.currentTimeMillis() - 20000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(System.currentTimeMillis() - 30000, "event", "realmId2", "clientId", "userId", "127.0.0.1", "error"));
provider.close(); provider.close();
provider = factory.create(); provider = factory.create();
provider.clear(); provider.clear("realmId");
Assert.assertEquals(0, provider.createQuery().getResultList().size()); Assert.assertEquals(1, provider.createQuery().getResultList().size());
} }
@Test @Test
@ -94,13 +97,14 @@ public abstract class AbstractAuditProviderTest {
provider.onEvent(create(System.currentTimeMillis() - 20000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create(System.currentTimeMillis() - 20000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
provider.onEvent(create(System.currentTimeMillis() - 30000, "event", "realmId2", "clientId", "userId", "127.0.0.1", "error"));
provider.close(); provider.close();
provider = factory.create(); provider = factory.create();
provider.clear(System.currentTimeMillis() - 10000); provider.clear("realmId", System.currentTimeMillis() - 10000);
Assert.assertEquals(2, provider.createQuery().getResultList().size()); Assert.assertEquals(3, provider.createQuery().getResultList().size());
} }
private Event create(String event, String realmId, String clientId, String userId, String ipAddress, String error) { private Event create(String event, String realmId, String clientId, String userId, String ipAddress, String error) {

View file

@ -0,0 +1,37 @@
package org.keycloak.representations.idm;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RealmAuditRepresentation {
protected boolean auditEnabled;
protected Long auditExpiration;
protected List<String> auditListeners;
public boolean isAuditEnabled() {
return auditEnabled;
}
public void setAuditEnabled(boolean auditEnabled) {
this.auditEnabled = auditEnabled;
}
public Long getAuditExpiration() {
return auditExpiration;
}
public void setAuditExpiration(Long auditExpiration) {
this.auditExpiration = auditExpiration;
}
public List<String> getAuditListeners() {
return auditListeners;
}
public void setAuditListeners(List<String> auditListeners) {
this.auditListeners = auditListeners;
}
}

View file

@ -1,6 +1,7 @@
package org.keycloak.representations.idm; package org.keycloak.representations.idm;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -47,6 +48,9 @@ public class RealmRepresentation {
protected List<AuthenticationProviderRepresentation> authenticationProviders; protected List<AuthenticationProviderRepresentation> authenticationProviders;
protected String loginTheme; protected String loginTheme;
protected String accountTheme; protected String accountTheme;
protected boolean auditEnabled;
protected long auditExpiration;
protected List<String> auditListeners;
public String getSelf() { public String getSelf() {
return self; return self;

View file

@ -208,6 +208,14 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
boolean removeRoleById(String id); boolean removeRoleById(String id);
boolean isAuditEnabled();
void setAuditEnabled(boolean enabled);
long getAuditExpiration();
void setAuditExpiration(long expiration);
Set<String> getAuditListeners(); Set<String> getAuditListeners();
void setAuditListeners(Set<String> listeners); void setAuditListeners(Set<String> listeners);

View file

@ -1184,6 +1184,28 @@ public class RealmAdapter implements RealmModel {
em.flush(); em.flush();
} }
@Override
public boolean isAuditEnabled() {
return realm.isAuditEnabled();
}
@Override
public void setAuditEnabled(boolean enabled) {
realm.setAuditEnabled(enabled);
em.flush();
}
@Override
public long getAuditExpiration() {
return realm.getAuditExpiration();
}
@Override
public void setAuditExpiration(long expiration) {
realm.setAuditExpiration(expiration);
em.flush();
}
@Override @Override
public Set<String> getAuditListeners() { public Set<String> getAuditListeners() {
return realm.getAuditListeners(); return realm.getAuditListeners();

View file

@ -99,8 +99,11 @@ public class RealmEntity {
@JoinTable(name="RealmDefaultRoles") @JoinTable(name="RealmDefaultRoles")
Collection<RoleEntity> defaultRoles = new ArrayList<RoleEntity>(); Collection<RoleEntity> defaultRoles = new ArrayList<RoleEntity>();
private boolean auditEnabled;
private long auditExpiration;
@ElementCollection @ElementCollection
protected Set<String> auditListeners= new HashSet<String>(); private Set<String> auditListeners= new HashSet<String>();
public String getId() { public String getId() {
return id; return id;
@ -349,6 +352,22 @@ public class RealmEntity {
this.bruteForceProtected = bruteForceProtected; this.bruteForceProtected = bruteForceProtected;
} }
public boolean isAuditEnabled() {
return auditEnabled;
}
public void setAuditEnabled(boolean auditEnabled) {
this.auditEnabled = auditEnabled;
}
public long getAuditExpiration() {
return auditExpiration;
}
public void setAuditExpiration(long auditExpiration) {
this.auditExpiration = auditExpiration;
}
public Set<String> getAuditListeners() { public Set<String> getAuditListeners() {
return auditListeners; return auditListeners;
} }

View file

@ -1141,6 +1141,28 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
updateRealm(); updateRealm();
} }
@Override
public boolean isAuditEnabled() {
return realm.isAuditEnabled();
}
@Override
public void setAuditEnabled(boolean enabled) {
realm.setAuditEnabled(enabled);
updateRealm();
}
@Override
public long getAuditExpiration() {
return realm.getAuditExpiration();
}
@Override
public void setAuditExpiration(long expiration) {
realm.setAuditExpiration(expiration);
updateRealm();
}
@Override @Override
public Set<String> getAuditListeners() { public Set<String> getAuditListeners() {
return new HashSet<String>(realm.getAuditListeners()); return new HashSet<String>(realm.getAuditListeners());

View file

@ -57,6 +57,8 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
private Map<String, String> socialConfig = new HashMap<String, String>(); private Map<String, String> socialConfig = new HashMap<String, String>();
private Map<String, String> ldapServerConfig; private Map<String, String> ldapServerConfig;
private boolean auditEnabled;
private long auditExpiration;
private List<String> auditListeners = new ArrayList<String>(); private List<String> auditListeners = new ArrayList<String>();
@MongoField @MongoField
@ -302,6 +304,24 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
this.ldapServerConfig = ldapServerConfig; this.ldapServerConfig = ldapServerConfig;
} }
@MongoField
public boolean isAuditEnabled() {
return auditEnabled;
}
public void setAuditEnabled(boolean auditEnabled) {
this.auditEnabled = auditEnabled;
}
@MongoField
public long getAuditExpiration() {
return auditExpiration;
}
public void setAuditExpiration(long auditExpiration) {
this.auditExpiration = auditExpiration;
}
@MongoField @MongoField
public List<String> getAuditListeners() { public List<String> getAuditListeners() {
return auditListeners; return auditListeners;

View file

@ -0,0 +1,57 @@
package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.keycloak.audit.Audit;
import org.keycloak.audit.AuditListener;
import org.keycloak.audit.AuditProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.services.ClientConnection;
import org.keycloak.services.ProviderSession;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AuditManager {
private Logger log = Logger.getLogger(AuditManager.class);
private RealmModel realm;
private ProviderSession providers;
private ClientConnection clientConnection;
public AuditManager(RealmModel realm, ProviderSession providers, ClientConnection clientConnection) {
this.realm = realm;
this.providers = providers;
this.clientConnection = clientConnection;
}
public Audit createAudit() {
List<AuditListener> listeners = new LinkedList<AuditListener>();
if (realm.isAuditEnabled()) {
AuditProvider auditProvider = providers.getProvider(AuditProvider.class);
if (auditProvider != null) {
listeners.add(auditProvider);
} else {
log.error("Audit enabled, but no audit provider configured");
}
}
if (realm.getAuditListeners() != null) {
for (String id : realm.getAuditListeners()) {
AuditListener listener = providers.getProvider(AuditListener.class, id);
if (listener != null) {
listeners.add(listener);
} else {
log.error("Audit listener '" + id + "' registered, but not found");
}
}
}
return new Audit(listeners, realm, clientConnection.getRemoteAddr());
}
}

View file

@ -13,6 +13,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.AuthenticationProviderRepresentation; import org.keycloak.representations.idm.AuthenticationProviderRepresentation;
import org.keycloak.representations.idm.ClaimRepresentation; import org.keycloak.representations.idm.ClaimRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmAuditRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
@ -20,6 +21,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -124,6 +126,20 @@ public class ModelToRepresentation {
return rep; return rep;
} }
public static RealmAuditRepresentation toAuditReprensetation(RealmModel realm) {
RealmAuditRepresentation rep = new RealmAuditRepresentation();
rep.setAuditEnabled(realm.isAuditEnabled());
if (realm.getAuditExpiration() != 0) {
rep.setAuditExpiration(realm.getAuditExpiration());
}
if (realm.getAuditListeners() != null) {
rep.setAuditListeners(new LinkedList<String>(realm.getAuditListeners()));
}
return rep;
}
public static CredentialRepresentation toRepresentation(UserCredentialModel cred) { public static CredentialRepresentation toRepresentation(UserCredentialModel cred) {
CredentialRepresentation rep = new CredentialRepresentation(); CredentialRepresentation rep = new CredentialRepresentation();
rep.setType(CredentialRepresentation.SECRET); rep.setType(CredentialRepresentation.SECRET);

View file

@ -24,6 +24,7 @@ import org.keycloak.representations.idm.AuthenticationLinkRepresentation;
import org.keycloak.representations.idm.AuthenticationProviderRepresentation; import org.keycloak.representations.idm.AuthenticationProviderRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.OAuthClientRepresentation; import org.keycloak.representations.idm.OAuthClientRepresentation;
import org.keycloak.representations.idm.RealmAuditRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.ScopeMappingRepresentation; import org.keycloak.representations.idm.ScopeMappingRepresentation;
@ -39,6 +40,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -166,6 +168,14 @@ public class RealmManager {
} }
} }
public void updateRealmAudit(RealmAuditRepresentation rep, RealmModel realm) {
realm.setAuditEnabled(rep.isAuditEnabled());
realm.setAuditExpiration(rep.getAuditExpiration());
if (rep.getAuditListeners() != null) {
realm.setAuditListeners(new HashSet<String>(rep.getAuditListeners()));
}
}
private void setupAdminManagement(RealmModel realm) { private void setupAdminManagement(RealmModel realm) {
RealmModel adminRealm; RealmModel adminRealm;
RoleModel adminRole; RoleModel adminRole;

View file

@ -10,6 +10,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.services.ClientConnection; import org.keycloak.services.ClientConnection;
import org.keycloak.services.ProviderSession; import org.keycloak.services.ProviderSession;
import org.keycloak.services.managers.AuditManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.SocialRequestManager;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
@ -68,7 +69,7 @@ public class RealmsResource {
public TokenService getTokenService(final @PathParam("realm") String name) { public TokenService getTokenService(final @PathParam("realm") String name) {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = locateRealm(name, realmManager); RealmModel realm = locateRealm(name, realmManager);
Audit audit = createAudit(realm); Audit audit = new AuditManager(realm, providers, clientConnection).createAudit();
TokenService tokenService = new TokenService(realm, tokenManager, audit); TokenService tokenService = new TokenService(realm, tokenManager, audit);
resourceContext.initResource(tokenService); resourceContext.initResource(tokenService);
return tokenService; return tokenService;
@ -93,7 +94,7 @@ public class RealmsResource {
throw new NotFoundException(); throw new NotFoundException();
} }
Audit audit = createAudit(realm); Audit audit = new AuditManager(realm, providers, clientConnection).createAudit();
AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager, audit); AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager, audit);
resourceContext.initResource(accountService); resourceContext.initResource(accountService);
accountService.init(); accountService.init();
@ -109,24 +110,4 @@ public class RealmsResource {
return realmResource; return realmResource;
} }
private Audit createAudit(RealmModel realm) {
List<AuditListener> listeners = new LinkedList<AuditListener>();
AuditProvider auditProvider = providers.getProvider(AuditProvider.class);
if (auditProvider != null) {
listeners.add(auditProvider);
}
if (realm.getAuditListeners() != null) {
for (String id : realm.getAuditListeners()) {
AuditListener listener = providers.getProvider(AuditListener.class, id);
if (listener != null) {
listeners.add(listener);
}
}
}
return new Audit(listeners, realm, clientConnection.getRemoteAddr());
}
} }

View file

@ -40,6 +40,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.ClientConnection; import org.keycloak.services.ClientConnection;
import org.keycloak.services.ProviderSession; import org.keycloak.services.ProviderSession;
import org.keycloak.services.managers.AuditManager;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.SocialRequestManager;
@ -129,7 +130,7 @@ public class SocialResource {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName); RealmModel realm = realmManager.getRealmByName(realmName);
Audit audit = createAudit(realm) Audit audit = new AuditManager(realm, providers, clientConnection).createAudit()
.event(Events.LOGIN) .event(Events.LOGIN)
.detail(Details.RESPONSE_TYPE, "code") .detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "social@" + provider.getId()); .detail(Details.AUTH_METHOD, "social@" + provider.getId());
@ -268,7 +269,7 @@ public class SocialResource {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName); RealmModel realm = realmManager.getRealmByName(realmName);
Audit audit = createAudit(realm) Audit audit = new AuditManager(realm, providers, clientConnection).createAudit()
.event(Events.LOGIN).client(clientId) .event(Events.LOGIN).client(clientId)
.detail(Details.REDIRECT_URI, redirectUri) .detail(Details.REDIRECT_URI, redirectUri)
.detail(Details.RESPONSE_TYPE, "code") .detail(Details.RESPONSE_TYPE, "code")
@ -335,24 +336,4 @@ public class SocialResource {
return queryParams; return queryParams;
} }
private Audit createAudit(RealmModel realm) {
List<AuditListener> listeners = new LinkedList<AuditListener>();
AuditProvider auditProvider = providers.getProvider(AuditProvider.class);
if (auditProvider != null) {
listeners.add(auditProvider);
}
if (realm.getAuditListeners() != null) {
for (String id : realm.getAuditListeners()) {
AuditListener listener = providers.getProvider(AuditListener.class, id);
if (listener != null) {
listeners.add(listener);
}
}
}
return new Audit(listeners, realm, clientConnection.getRemoteAddr());
}
} }

View file

@ -9,6 +9,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.idm.RealmAuditRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.ProviderSession; import org.keycloak.services.ProviderSession;
import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.ModelToRepresentation;
@ -148,7 +149,26 @@ public class RealmAdminResource {
return stats; return stats;
} }
@GET
@Path("audit") @Path("audit")
@Produces("application/json")
public RealmAuditRepresentation getRealmAudit() {
auth.init(RealmAuth.Resource.AUDIT).requireView();
return ModelToRepresentation.toAuditReprensetation(realm);
}
@PUT
@Path("audit")
@Consumes("application/json")
public void updateRealmAudit(final RealmAuditRepresentation rep) {
auth.init(RealmAuth.Resource.AUDIT).requireManage();
logger.debug("updating realm audit: " + realm.getName());
new RealmManager(session).updateRealmAudit(rep, realm);
}
@Path("audit/events")
@GET @GET
@NoCache @NoCache
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@ -181,4 +201,12 @@ public class RealmAdminResource {
return query.getResultList(); return query.getResultList();
} }
@Path("audit/events")
@DELETE
public void clearAudit() {
auth.init(RealmAuth.Resource.AUDIT).requireManage();
AuditProvider audit = providers.getProvider(AuditProvider.class);
audit.clear(realm.getId());
}
} }

View file

@ -1,30 +1,38 @@
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import org.keycloak.audit.AuditListener;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider; import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.services.ProviderSession;
import org.keycloak.social.SocialProvider; import org.keycloak.social.SocialProvider;
import org.keycloak.spi.authentication.AuthenticationProvider; import org.keycloak.spi.authentication.AuthenticationProvider;
import org.keycloak.spi.authentication.AuthenticationProviderManager; import org.keycloak.spi.authentication.AuthenticationProviderManager;
import org.keycloak.util.ProviderLoader; import org.keycloak.util.ProviderLoader;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.core.Context;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class ServerInfoAdminResource { public class ServerInfoAdminResource {
@Context
private ProviderSession providers;
@GET @GET
public ServerInfoRepresentation getInfo() { public ServerInfoRepresentation getInfo() {
ServerInfoRepresentation info = new ServerInfoRepresentation(); ServerInfoRepresentation info = new ServerInfoRepresentation();
setSocialProviders(info); setSocialProviders(info);
setThemes(info); setThemes(info);
setAuthProviders(info); setAuthProviders(info);
setAuditListeners(info);
return info; return info;
} }
@ -57,6 +65,15 @@ public class ServerInfoAdminResource {
} }
} }
private void setAuditListeners(ServerInfoRepresentation info) {
info.auditListeners = new LinkedList<String>();
Set<String> providers = this.providers.listProviderIds(AuditListener.class);
if (providers != null) {
info.auditListeners.addAll(providers);
}
}
public static class ServerInfoRepresentation { public static class ServerInfoRepresentation {
private Map<String, List<String>> themes; private Map<String, List<String>> themes;
@ -65,6 +82,8 @@ public class ServerInfoAdminResource {
private Map<String, List<String>> authProviders; private Map<String, List<String>> authProviders;
private List<String> auditListeners;
public ServerInfoRepresentation() { public ServerInfoRepresentation() {
} }
@ -79,6 +98,10 @@ public class ServerInfoAdminResource {
public Map<String, List<String>> getAuthProviders() { public Map<String, List<String>> getAuthProviders() {
return authProviders; return authProviders;
} }
public List<String> getAuditListeners() {
return auditListeners;
}
} }
} }

View file

@ -334,41 +334,56 @@ public class AccountTest {
@Test @Test
public void viewLog() { public void viewLog() {
List<Event> e = new LinkedList<Event>(); keycloakRule.configure(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setAuditEnabled(true);
}
});
loginPage.open(); try {
loginPage.clickRegister(); List<Event> e = new LinkedList<Event>();
registerPage.register("view", "log", "view-log@localhost", "view-log", "password", "password"); loginPage.open();
loginPage.clickRegister();
e.add(events.poll()); registerPage.register("view", "log", "view-log@localhost", "view-log", "password", "password");
e.add(events.poll());
profilePage.open(); e.add(events.poll());
profilePage.updateProfile("view", "log2", "view-log@localhost"); e.add(events.poll());
e.add(events.poll()); profilePage.open();
profilePage.updateProfile("view", "log2", "view-log@localhost");
logPage.open(); e.add(events.poll());
e.add(events.poll()); logPage.open();
Collections.reverse(e); e.add(events.poll());
Assert.assertTrue(logPage.isCurrent()); Collections.reverse(e);
List<List<String>> actual = logPage.getEvents(); Assert.assertTrue(logPage.isCurrent());
Assert.assertEquals(e.size(), actual.size()); List<List<String>> actual = logPage.getEvents();
Iterator<List<String>> itr = actual.iterator(); Assert.assertEquals(e.size(), actual.size());
for (Event event : e) {
List<String> a = itr.next(); Iterator<List<String>> itr = actual.iterator();
Assert.assertEquals(event.getEvent().replace('_', ' '), a.get(1)); for (Event event : e) {
Assert.assertEquals(event.getIpAddress(), a.get(2)); List<String> a = itr.next();
Assert.assertEquals(event.getClientId(), a.get(3)); Assert.assertEquals(event.getEvent().replace('_', ' '), a.get(1));
Assert.assertEquals(event.getIpAddress(), a.get(2));
Assert.assertEquals(event.getClientId(), a.get(3));
}
} finally {
keycloakRule.configure(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setAuditEnabled(false);
}
});
} }
} }
} }